From 73946b05450851be7a63a05dac881586f6ebb3ea Mon Sep 17 00:00:00 2001 From: Ivor Smorenburg Date: Sun, 15 Sep 2024 15:49:45 +0100 Subject: [PATCH 1/9] First refactor widgets --- app/src/main/AndroidManifest.xml | 10 +- .../android/HomeAssistantApplication.kt | 3 + .../widgets/BaseWidgetConfigureActivity.kt | 7 +- .../android/widgets/BaseWidgetProvider.kt | 192 +++++++++----- .../android/widgets/button/ButtonWidget.kt | 251 ++++++------------ .../button/ButtonWidgetConfigureActivity.kt | 16 +- .../android/widgets/camera/CameraWidget.kt | 227 ++++++---------- .../camera/CameraWidgetConfigureActivity.kt | 14 +- .../android/widgets/entity/EntityWidget.kt | 54 ++-- .../entity/EntityWidgetConfigureActivity.kt | 11 +- .../mediaplayer/MediaPlayerControlsWidget.kt | 187 +++++-------- ...iaPlayerControlsWidgetConfigureActivity.kt | 14 +- .../widgets/template/TemplateWidget.kt | 215 +++------------ .../TemplateWidgetConfigureActivity.kt | 14 +- app/src/main/res/layout/widget_camera.xml | 41 +-- .../main/res/layout/widget_media_controls.xml | 51 ++-- automotive/src/main/AndroidManifest.xml | 10 +- .../47.json | 21 +- .../android/common/data/DataModule.kt | 7 +- .../repositories/BaseDaoWidgetRepository.kt | 25 ++ .../repositories/ButtonWidgetRepository.kt | 5 + .../ButtonWidgetRepositoryImpl.kt | 29 ++ .../repositories/CameraWidgetRepository.kt | 5 + .../CameraWidgetRepositoryImpl.kt | 25 ++ .../MediaPlayerControlsWidgetRepository.kt | 5 + ...MediaPlayerControlsWidgetRepositoryImpl.kt | 27 ++ .../data/repositories/RepositoryModule.kt | 36 +++ .../repositories/StaticWidgetRepository.kt | 5 + .../StaticWidgetRepositoryImpl.kt | 27 ++ .../repositories/TemplateWidgetRepository.kt | 5 + .../TemplateWidgetRepositoryImpl.kt | 27 ++ .../database/widget/ButtonWidgetEntity.kt | 2 + .../database/widget/CameraWidgetEntity.kt | 2 +- .../widget/MediaPlayerControlsWidgetEntity.kt | 2 +- .../database/widget/StaticWidgetEntity.kt | 2 +- .../database/widget/TemplateWidgetEntity.kt | 2 + .../android/database/widget/WidgetEntity.kt | 1 + .../companion/android/util/DisplayUtils.kt | 10 + 38 files changed, 765 insertions(+), 822 deletions(-) create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/BaseDaoWidgetRepository.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepository.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepositoryImpl.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepository.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepositoryImpl.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepository.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepositoryImpl.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/RepositoryModule.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepository.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepositoryImpl.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepository.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepositoryImpl.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/util/DisplayUtils.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a4bf9ef0e92..3e55c88586a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -112,7 +112,7 @@ - + - + @@ -137,7 +137,7 @@ android:exported="false"> - + - + @@ -174,7 +174,7 @@ - + > : 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 diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt index 275ec96eda9..a3d996bb76c 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt @@ -6,32 +6,40 @@ 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, 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>() val widgetJobs = mutableMapOf() } @@ -39,7 +47,10 @@ abstract class BaseWidgetProvider : AppWidgetProvider() { @Inject lateinit var serverManager: ServerManager - private var thisSetScope = false + @Inject + lateinit var repository: T + + protected var thisSetScope = false protected var lastIntent = "" init { @@ -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>> = + 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( @@ -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 } @@ -129,8 +151,11 @@ abstract class BaseWidgetProvider : AppWidgetProvider() { } } + abstract suspend fun getUpdates(serverId: Int, entityIds: List): Flow? + private fun onScreenOff() { if (thisSetScope) { + widgetWorkScope?.cancel() widgetScope?.cancel() thisSetScope = false widgetEntities.clear() @@ -138,51 +163,88 @@ abstract class BaseWidgetProvider : AppWidgetProvider() { } } - 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>? = 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>> abstract fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) - abstract suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity<*>) } diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt index 64d4b9a76a0..80f924617db 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt @@ -2,16 +2,15 @@ package io.homeassistant.companion.android.widgets.button import android.app.PendingIntent import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider import android.content.ComponentName import android.content.Context import android.content.Intent -import android.graphics.Color import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Log import android.view.View +import android.view.View.VISIBLE import android.widget.RemoteViews import android.widget.Toast import androidx.core.content.ContextCompat @@ -30,30 +29,26 @@ import com.mikepenz.iconics.utils.size import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.R as commonR -import io.homeassistant.companion.android.common.data.servers.ServerManager -import io.homeassistant.companion.android.database.widget.ButtonWidgetDao +import io.homeassistant.companion.android.common.data.integration.Entity +import io.homeassistant.companion.android.common.data.repositories.ButtonWidgetRepository import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.util.getAttribute import io.homeassistant.companion.android.util.icondialog.getIconByMdiName +import io.homeassistant.companion.android.widgets.BaseWidgetProvider import io.homeassistant.companion.android.widgets.common.WidgetAuthenticationActivity import java.util.regex.Pattern -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @AndroidEntryPoint -class ButtonWidget : AppWidgetProvider() { +class ButtonWidget : BaseWidgetProvider>>() { companion object { private const val TAG = "ButtonWidget" const val CALL_SERVICE = "io.homeassistant.companion.android.widgets.button.ButtonWidget.CALL_SERVICE" private const val CALL_SERVICE_AUTH = "io.homeassistant.companion.android.widgets.button.ButtonWidget.CALL_SERVICE_AUTH" - internal const val RECEIVE_DATA = - "io.homeassistant.companion.android.widgets.button.ButtonWidget.RECEIVE_DATA" internal const val EXTRA_SERVER_ID = "EXTRA_SERVER_ID" internal const val EXTRA_DOMAIN = "EXTRA_DOMAIN" @@ -69,108 +64,15 @@ class ButtonWidget : AppWidgetProvider() { private const val DEFAULT_MAX_ICON_SIZE = 512 } - @Inject - lateinit var serverManager: ServerManager + override fun getWidgetProvider(context: Context): ComponentName = ComponentName(context, ButtonWidget::class.java) - @Inject - lateinit var buttonWidgetDao: ButtonWidgetDao - - private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) - - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { - // There may be multiple widgets active, so update all of them - for (appWidgetId in appWidgetIds) { - mainScope.launch { - val views = getWidgetRemoteViews(context, appWidgetId) - appWidgetManager.updateAppWidget(appWidgetId, views) - } - } - } - - private fun updateAllWidgets(context: Context) { - mainScope.launch { - val appWidgetManager = AppWidgetManager.getInstance(context) - val systemWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, ButtonWidget::class.java)) - val dbWidgetList = buttonWidgetDao.getAll() - - val invalidWidgetIds = dbWidgetList - .filter { !systemWidgetIds.contains(it.id) } - .map { it.id } - if (invalidWidgetIds.isNotEmpty()) { - Log.i(TAG, "Found widgets $invalidWidgetIds in database, but not in AppWidgetManager - sending onDeleted") - onDeleted(context, invalidWidgetIds.toIntArray()) - } - - val buttonWidgetEntityList = dbWidgetList.filter { systemWidgetIds.contains(it.id) } - if (buttonWidgetEntityList.isNotEmpty()) { - Log.d(TAG, "Updating all widgets") - for (item in buttonWidgetEntityList) { - val views = getWidgetRemoteViews(context, item.id) - - setLabelVisibility(views, item) - views.setViewVisibility(R.id.widgetProgressBar, View.INVISIBLE) - views.setViewVisibility(R.id.widgetImageButtonLayout, View.VISIBLE) - appWidgetManager.updateAppWidget(item.id, views) - } - } - } - } - - override fun onDeleted(context: Context, appWidgetIds: IntArray) { - // When the user deletes the widget, delete the preference associated with it. - mainScope.launch { - buttonWidgetDao.deleteAll(appWidgetIds) - } - } - - override fun onEnabled(context: Context) { - // Enter relevant functionality for when the first widget is created - } - - override fun onDisabled(context: Context) { - // Enter relevant functionality for when the last widget is disabled - } - - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action - val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) - - Log.d( - TAG, - "Broadcast received: " + System.lineSeparator() + - "Broadcast action: " + action + System.lineSeparator() + - "AppWidgetId: " + appWidgetId - ) - - super.onReceive(context, intent) - when (action) { - CALL_SERVICE_AUTH -> authThenCallConfiguredAction(context, appWidgetId) - CALL_SERVICE -> callConfiguredAction(context, appWidgetId) - RECEIVE_DATA -> saveActionCallConfiguration(context, intent.extras, appWidgetId) - Intent.ACTION_SCREEN_ON -> updateAllWidgets(context) - } - } - - private fun authThenCallConfiguredAction(context: Context, appWidgetId: Int) { - Log.d(TAG, "Calling authentication, then configured action") - - val intent = Intent(context, WidgetAuthenticationActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) - context.startActivity(intent) - } - - private fun getWidgetRemoteViews(context: Context, appWidgetId: Int): RemoteViews { + override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, hasActiveConnection: Boolean, suggestedEntity: Entity>?): RemoteViews { // Every time AppWidgetManager.updateAppWidget(...) is called, the button listener // and label need to be re-assigned, or the next time the layout updates // (e.g home screen rotation) the widget will fall back on its default layout // without any click listener being applied - val widget = buttonWidgetDao.get(appWidgetId) + val widget = repository.get(appWidgetId) val auth = widget?.requireAuthentication == true val intent = Intent(context, ButtonWidget::class.java).apply { @@ -186,7 +88,8 @@ class ButtonWidget : AppWidgetProvider() { widget.textColor?.let { textColor = it.toColorInt() } setTextColor(R.id.widgetLabel, textColor) } - setWidgetBackground(this, widget) + + setWidgetBackground(this, R.id.widgetLayout, widget) // Label setLabelVisibility(this, widget) @@ -244,17 +147,81 @@ class ButtonWidget : AppWidgetProvider() { } } - private fun setWidgetBackground(views: RemoteViews, widget: ButtonWidgetEntity?) { - when (widget?.backgroundType) { - WidgetBackgroundType.TRANSPARENT -> { - views.setInt(R.id.widgetLayout, "setBackgroundColor", Color.TRANSPARENT) - } - else -> { - views.setInt(R.id.widgetLayout, "setBackgroundResource", R.drawable.widget_button_background) - } + override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { + if (extras == null) return + + val serverId = if (extras.containsKey(EXTRA_SERVER_ID)) extras.getInt(EXTRA_SERVER_ID) else null + val domain: String? = extras.getString(EXTRA_DOMAIN) + val action: String? = extras.getString(EXTRA_ACTION) + val actionData: String? = extras.getString(EXTRA_ACTION_DATA) + val label: String? = extras.getString(EXTRA_LABEL) + val requireAuthentication: Boolean = extras.getBoolean(EXTRA_REQUIRE_AUTHENTICATION) + val icon: String = extras.getString(EXTRA_ICON_NAME) ?: "mdi:flash" + val backgroundType = BundleCompat.getSerializable(extras, EXTRA_BACKGROUND_TYPE, WidgetBackgroundType::class.java) + ?: WidgetBackgroundType.DAYNIGHT + val textColor: String? = extras.getString(EXTRA_TEXT_COLOR) + + if (serverId == null || domain == null || action == null || actionData == null) { + Log.e(TAG, "Did not receive complete action call data") + return + } + + widgetScope?.launch { + Log.d( + TAG, + "Saving action call config data:" + System.lineSeparator() + + "domain: " + domain + System.lineSeparator() + + "action: " + action + System.lineSeparator() + + "action_data: " + actionData + System.lineSeparator() + + "require_authentication: " + requireAuthentication + System.lineSeparator() + + "label: " + label + ) + + val widget = ButtonWidgetEntity( + appWidgetId, + serverId, + null, + icon, + domain, + action, + actionData, + label, + backgroundType, + textColor, + requireAuthentication + ) + + repository.add(widget) + + // It is the responsibility of the configuration activity to update the app widget + // This method is only called during the initial setup of the widget, + // so rather than duplicating code in the ButtonWidgetConfigurationActivity, + // it is just calling onUpdate manually here. + // onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId)) + } + } + + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + super.onReceive(context, intent) + when (action) { + CALL_SERVICE_AUTH -> authThenCallConfiguredAction(context, appWidgetId) + CALL_SERVICE -> callConfiguredAction(context, appWidgetId) } } + override suspend fun getUpdates(serverId: Int, entityIds: List): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> + + private fun authThenCallConfiguredAction(context: Context, appWidgetId: Int) { + Log.d(TAG, "Calling authentication, then configured action") + + val intent = Intent(context, WidgetAuthenticationActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + context.startActivity(intent) + } + private fun setLabelVisibility(views: RemoteViews, widget: ButtonWidgetEntity?) { val labelVisibility = if (widget?.label.isNullOrBlank()) View.GONE else View.VISIBLE views.setViewVisibility(R.id.widgetLabelLayout, labelVisibility) @@ -272,9 +239,9 @@ class ButtonWidget : AppWidgetProvider() { loadingViews.setViewVisibility(R.id.widgetImageButtonLayout, View.GONE) appWidgetManager.partiallyUpdateAppWidget(appWidgetId, loadingViews) - val widget = buttonWidgetDao.get(appWidgetId) + val widget = repository.get(appWidgetId) - mainScope.launch { + widgetScope?.launch { // Set default feedback as negative var feedbackColor = R.drawable.widget_button_background_red var feedbackIcon = R.drawable.ic_clear_black @@ -338,59 +305,13 @@ class ButtonWidget : AppWidgetProvider() { feedbackViews.setViewVisibility(R.id.widgetImageButtonLayout, View.VISIBLE) appWidgetManager.partiallyUpdateAppWidget(appWidgetId, feedbackViews) - // Reload default views in the coroutine to pass to the post handler - val views = getWidgetRemoteViews(context, appWidgetId) - // Set a timer to change it back after 1 second Handler(Looper.getMainLooper()).postDelayed( { - setLabelVisibility(views, widget) - setWidgetBackground(views, widget) - appWidgetManager.updateAppWidget(appWidgetId, views) + forceUpdateView(context, appWidgetId, appWidgetManager) }, 1000 ) } } - - private fun saveActionCallConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { - if (extras == null) return - - val serverId = if (extras.containsKey(EXTRA_SERVER_ID)) extras.getInt(EXTRA_SERVER_ID) else null - val domain: String? = extras.getString(EXTRA_DOMAIN) - val action: String? = extras.getString(EXTRA_ACTION) - val actionData: String? = extras.getString(EXTRA_ACTION_DATA) - val label: String? = extras.getString(EXTRA_LABEL) - val requireAuthentication: Boolean = extras.getBoolean(EXTRA_REQUIRE_AUTHENTICATION) - val icon: String = extras.getString(EXTRA_ICON_NAME) ?: "mdi:flash" - val backgroundType = BundleCompat.getSerializable(extras, EXTRA_BACKGROUND_TYPE, WidgetBackgroundType::class.java) - ?: WidgetBackgroundType.DAYNIGHT - val textColor: String? = extras.getString(EXTRA_TEXT_COLOR) - - if (serverId == null || domain == null || action == null || actionData == null) { - Log.e(TAG, "Did not receive complete action call data") - return - } - - mainScope.launch { - Log.d( - TAG, - "Saving action call config data:" + System.lineSeparator() + - "domain: " + domain + System.lineSeparator() + - "action: " + action + System.lineSeparator() + - "action_data: " + actionData + System.lineSeparator() + - "require_authentication: " + requireAuthentication + System.lineSeparator() + - "label: " + label - ) - - val widget = ButtonWidgetEntity(appWidgetId, serverId, icon, domain, action, actionData, label, backgroundType, textColor, requireAuthentication) - buttonWidgetDao.add(widget) - - // It is the responsibility of the configuration activity to update the app widget - // This method is only called during the initial setup of the widget, - // so rather than duplicating code in the ButtonWidgetConfigurationActivity, - // it is just calling onUpdate manually here. - onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId)) - } - } } diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt index 943983c12c9..428a4873b14 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt @@ -36,7 +36,7 @@ import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.data.integration.Action import io.homeassistant.companion.android.common.data.integration.Entity -import io.homeassistant.companion.android.database.widget.ButtonWidgetDao +import io.homeassistant.companion.android.common.data.repositories.ButtonWidgetRepository import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.databinding.WidgetButtonConfigureBinding import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel @@ -45,24 +45,20 @@ import io.homeassistant.companion.android.util.icondialog.IconDialogFragment import io.homeassistant.companion.android.util.icondialog.getIconByMdiName import io.homeassistant.companion.android.util.icondialog.mdiName import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity +import io.homeassistant.companion.android.widgets.BaseWidgetProvider.Companion.RECEIVE_DATA import io.homeassistant.companion.android.widgets.common.ActionFieldBinder import io.homeassistant.companion.android.widgets.common.SingleItemArrayAdapter import io.homeassistant.companion.android.widgets.common.WidgetDynamicFieldAdapter import io.homeassistant.companion.android.widgets.common.WidgetUtils -import javax.inject.Inject import kotlinx.coroutines.launch @AndroidEntryPoint -class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity() { +class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity() { companion object { private const val TAG: String = "ButtonWidgetConfigAct" private const val PIN_WIDGET_CALLBACK = "io.homeassistant.companion.android.widgets.button.ButtonWidgetConfigureActivity.PIN_WIDGET_CALLBACK" } - @Inject - lateinit var buttonWidgetDao: ButtonWidgetDao - override val dao get() = buttonWidgetDao - private var actions = mutableMapOf>() private var entities = mutableMapOf>>() private var dynamicFields = ArrayList() @@ -139,7 +135,7 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity() { val existingActionData = mutableMapOf() val addedFields = mutableListOf() - buttonWidgetDao.get(appWidgetId)?.let { buttonWidget -> + repository.get(appWidgetId)?.let { buttonWidget -> if ( buttonWidget.serverId != selectedServerId || "${buttonWidget.domain}.${buttonWidget.service}" != actionText @@ -224,7 +220,7 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity() { return } - val buttonWidget = buttonWidgetDao.get(appWidgetId) + val buttonWidget = repository.get(appWidgetId) val backgroundTypeValues = WidgetUtils.getBackgroundOptionList(this) binding.backgroundType.adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, backgroundTypeValues) @@ -410,7 +406,7 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity() { // Set up a broadcast intent and pass the action data as extras val intent = Intent() - intent.action = ButtonWidget.RECEIVE_DATA + intent.action = RECEIVE_DATA intent.component = ComponentName(context, ButtonWidget::class.java) intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt index 4ee8a27e813..6c6a709fcd6 100755 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt @@ -2,14 +2,11 @@ package io.homeassistant.companion.android.widgets.camera import android.app.PendingIntent import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.res.Resources +import android.graphics.Bitmap import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.util.Log import android.view.View import android.widget.RemoteViews @@ -18,104 +15,45 @@ import com.squareup.picasso.Picasso import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.BuildConfig import io.homeassistant.companion.android.R -import io.homeassistant.companion.android.common.data.servers.ServerManager -import io.homeassistant.companion.android.database.widget.CameraWidgetDao +import io.homeassistant.companion.android.common.data.integration.Entity +import io.homeassistant.companion.android.common.data.repositories.CameraWidgetRepository import io.homeassistant.companion.android.database.widget.CameraWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetTapAction -import io.homeassistant.companion.android.util.hasActiveConnection +import io.homeassistant.companion.android.util.DisplayUtils.getScreenWidth import io.homeassistant.companion.android.webview.WebViewActivity -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import io.homeassistant.companion.android.widgets.BaseWidgetProvider +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @AndroidEntryPoint -class CameraWidget : AppWidgetProvider() { +class CameraWidget : BaseWidgetProvider>() { companion object { private const val TAG = "CameraWidget" - internal const val RECEIVE_DATA = - "io.homeassistant.companion.android.widgets.camera.CameraWidget.RECEIVE_DATA" + internal const val UPDATE_IMAGE = "io.homeassistant.companion.android.widgets.camera.CameraWidget.UPDATE_IMAGE" + internal const val ENTITY_PICTURE_ATTRIBUTE = "entity_picture" + internal const val EXTRA_SERVER_ID = "EXTRA_SERVER_ID" internal const val EXTRA_ENTITY_ID = "EXTRA_ENTITY_ID" internal const val EXTRA_TAP_ACTION = "EXTRA_TAP_ACTION" - private var lastIntent = "" - } - - @Inject - lateinit var serverManager: ServerManager - - @Inject - lateinit var cameraWidgetDao: CameraWidgetDao - - private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) - - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { - // There may be multiple widgets active, so update all of them - appWidgetIds.forEach { appWidgetId -> - updateAppWidget( - context, - appWidgetId, - appWidgetManager - ) - } } - private fun updateAppWidget( - context: Context, - appWidgetId: Int, - appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context) - ) { - if (!context.hasActiveConnection()) { - Log.d(TAG, "Skipping widget update since network connection is not active") - return - } - mainScope.launch { - val views = getWidgetRemoteViews(context, appWidgetId) - appWidgetManager.updateAppWidget(appWidgetId, views) - } - } + private var lastCameraBitmap: Bitmap? = null - private fun updateAllWidgets(context: Context) { - mainScope.launch { - val appWidgetManager = AppWidgetManager.getInstance(context) - val systemWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, CameraWidget::class.java)) - val dbWidgetList = cameraWidgetDao.getAll() + override fun getWidgetProvider(context: Context): ComponentName = + ComponentName(context, CameraWidget::class.java) - val invalidWidgetIds = dbWidgetList - .filter { !systemWidgetIds.contains(it.id) } - .map { it.id } - if (invalidWidgetIds.isNotEmpty()) { - Log.i(TAG, "Found widgets $invalidWidgetIds in database, but not in AppWidgetManager - sending onDeleted") - onDeleted(context, invalidWidgetIds.toIntArray()) - } - - val cameraWidgetList = dbWidgetList.filter { systemWidgetIds.contains(it.id) } - if (cameraWidgetList.isNotEmpty()) { - Log.d(TAG, "Updating all widgets") - for (item in cameraWidgetList) { - updateAppWidget(context, item.id, appWidgetManager) - } - } - } - } - - private suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int): RemoteViews { + override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, hasActiveConnection: Boolean, suggestedEntity: Entity<*>?): RemoteViews { val updateCameraIntent = Intent(context, CameraWidget::class.java).apply { action = UPDATE_IMAGE putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) } return RemoteViews(context.packageName, R.layout.widget_camera).apply { - val widget = cameraWidgetDao.get(appWidgetId) + val widget = repository.get(appWidgetId) if (widget != null) { var entityPictureUrl: String? try { @@ -129,18 +67,18 @@ class CameraWidget : AppWidgetProvider() { val baseUrl = serverManager.getServer(widget.serverId)?.connection?.getUrl().toString().removeSuffix("/") val url = "$baseUrl$entityPictureUrl" if (entityPictureUrl == null) { - setImageViewResource( - R.id.widgetCameraImage, - R.drawable.app_icon_round - ) - setViewVisibility( - R.id.widgetCameraPlaceholder, - View.VISIBLE - ) - setViewVisibility( - R.id.widgetCameraImage, - View.GONE - ) +// setImageViewResource( +// R.id.widgetCameraImage, +// android.R.drawable.ic_menu_camera +// ) +// setViewVisibility( +// R.id.widgetCameraPlaceholder, +// View.VISIBLE +// ) +// setViewVisibility( +// R.id.widgetCameraImage, +// View.GONE +// ) } else { setViewVisibility( R.id.widgetCameraImage, @@ -151,25 +89,10 @@ class CameraWidget : AppWidgetProvider() { View.GONE ) Log.d(TAG, "Fetching camera image") - Handler(Looper.getMainLooper()).post { - val picasso = Picasso.get() - if (BuildConfig.DEBUG) { - picasso.isLoggingEnabled = true - } - try { - picasso.invalidate(url) - picasso.load(url).resize(getScreenWidth(), 0).onlyScaleDown().into( - this, - R.id.widgetCameraImage, - intArrayOf(appWidgetId) - ) - } catch (e: Exception) { - Log.e(TAG, "Unable to fetch image", e) - } - Log.d(TAG, "Fetch and load complete") - } } + updateBitmapCameraImage(context, hasActiveConnection, this, appWidgetId, url) + val tapWidgetPendingIntent = when (widget.tapAction) { WidgetTapAction.OPEN -> PendingIntent.getActivity( context, @@ -185,7 +108,15 @@ class CameraWidget : AppWidgetProvider() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } - setOnClickPendingIntent(R.id.widgetCameraImage, tapWidgetPendingIntent) + setOnClickPendingIntent( + R.id.widgetLayout, + PendingIntent.getBroadcast( + context, + appWidgetId, + updateCameraIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + ) setOnClickPendingIntent(R.id.widgetCameraPlaceholder, tapWidgetPendingIntent) } } @@ -193,29 +124,18 @@ class CameraWidget : AppWidgetProvider() { private suspend fun retrieveCameraImageUrl(serverId: Int, entityId: String): String? { val entity = serverManager.integrationRepository(serverId).getEntity(entityId) - return entity?.attributes?.get("entity_picture")?.toString() + return entity?.attributes?.get(ENTITY_PICTURE_ATTRIBUTE)?.toString() } override fun onReceive(context: Context, intent: Intent) { - lastIntent = intent.action.toString() - val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) - - Log.d( - TAG, - "Broadcast received: " + System.lineSeparator() + - "Broadcast action: " + lastIntent + System.lineSeparator() + - "AppWidgetId: " + appWidgetId - ) - super.onReceive(context, intent) + val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) when (lastIntent) { - RECEIVE_DATA -> saveEntityConfiguration(context, intent.extras, appWidgetId) - UPDATE_IMAGE -> updateAppWidget(context, appWidgetId) - Intent.ACTION_SCREEN_ON -> updateAllWidgets(context) + UPDATE_IMAGE -> forceUpdateView(context, appWidgetId) } } - private fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { + override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { if (extras == null) return val serverSelection = if (extras.containsKey(EXTRA_SERVER_ID)) extras.getInt(EXTRA_SERVER_ID) else null @@ -228,41 +148,60 @@ class CameraWidget : AppWidgetProvider() { return } - mainScope.launch { + widgetScope?.launch { Log.d( TAG, "Saving camera config data:" + System.lineSeparator() + "entity id: " + entitySelection + System.lineSeparator() ) - cameraWidgetDao.add( + repository.add( CameraWidgetEntity( - appWidgetId, - serverSelection, - entitySelection, - tapActionSelection + id = appWidgetId, + entityId = entitySelection, + serverId = serverSelection, + tapAction = tapActionSelection ) ) - - onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId)) } } - override fun onDeleted(context: Context, appWidgetIds: IntArray) { - // When the user deletes the widget, delete the preference associated with it. - mainScope.launch { - cameraWidgetDao.deleteAll(appWidgetIds) + private fun updateBitmapCameraImage(context: Context, hasActiveConnection: Boolean, views: RemoteViews, appWidgetId: Int, url: String) { + if (hasActiveConnection) { + widgetWorkScope?.launch { + val picasso = Picasso.get() + picasso.isLoggingEnabled = BuildConfig.DEBUG + try { + picasso.invalidate(url) + lastCameraBitmap = picasso.load(url) + .stableKey(url) + .resize(getScreenWidth(), 0) + .onlyScaleDown().get() + + widgetScope?.launch { + views.setImageViewBitmap(R.id.widgetCameraImage, lastCameraBitmap) + AppWidgetManager.getInstance(context).partiallyUpdateAppWidget(appWidgetId, views) + } + } catch (e: Exception) { + Log.e(TAG, "Unable to fetch image", e) + } + Log.d(TAG, "Fetch and load complete") + } + } else { + widgetScope?.launch { + lastCameraBitmap?.let { + views.setImageViewBitmap(R.id.widgetCameraImage, it) + AppWidgetManager.getInstance(context).partiallyUpdateAppWidget(appWidgetId, views) + } + } } } - override fun onEnabled(context: Context) { - // Enter relevant functionality for when the first widget is created - } - - override fun onDisabled(context: Context) { - // Enter relevant functionality for when the last widget is disabled - } +// override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity<*>) { +// super.onEntityStateChanged(context, appWidgetId, entity) +// widgetScope?.launch { +// forceUpdateView(context, appWidgetId) +// } +// } - private fun getScreenWidth(): Int { - return Resources.getSystem().displayMetrics.widthPixels - } + override suspend fun getUpdates(serverId: Int, entityIds: List): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> } diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt index ecd7bd13497..4d5e23eaca3 100755 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt @@ -20,18 +20,18 @@ import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.domain -import io.homeassistant.companion.android.database.widget.CameraWidgetDao +import io.homeassistant.companion.android.common.data.repositories.CameraWidgetRepository import io.homeassistant.companion.android.database.widget.WidgetTapAction import io.homeassistant.companion.android.databinding.WidgetCameraConfigureBinding import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity +import io.homeassistant.companion.android.widgets.BaseWidgetProvider.Companion.RECEIVE_DATA import io.homeassistant.companion.android.widgets.common.SingleItemArrayAdapter -import javax.inject.Inject import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @AndroidEntryPoint -class CameraWidgetConfigureActivity : BaseWidgetConfigureActivity() { +class CameraWidgetConfigureActivity : BaseWidgetConfigureActivity() { companion object { private const val TAG: String = "CameraWidgetConfigAct" @@ -51,10 +51,6 @@ class CameraWidgetConfigureActivity : BaseWidgetConfigureActivity() { private var entities = mutableMapOf>>() private var selectedEntity: Entity? = null - @Inject - lateinit var cameraWidgetDao: CameraWidgetDao - override val dao get() = cameraWidgetDao - private var entityAdapter: SingleItemArrayAdapter>? = null public override fun onCreate(savedInstanceState: Bundle?) { @@ -109,7 +105,7 @@ class CameraWidgetConfigureActivity : BaseWidgetConfigureActivity() { } initTapActionsSpinner() - val cameraWidget = cameraWidgetDao.get(appWidgetId) + val cameraWidget = repository.get(appWidgetId) if (cameraWidget != null) { setCurrentTapAction(tapAction = cameraWidget.tapAction) binding.widgetTextConfigEntityId.setText(cameraWidget.entityId) @@ -191,7 +187,7 @@ class CameraWidgetConfigureActivity : BaseWidgetConfigureActivity() { // Set up a broadcast intent and pass the service call data as extras val intent = Intent() - intent.action = CameraWidget.RECEIVE_DATA + intent.action = RECEIVE_DATA intent.component = ComponentName(context, CameraWidget::class.java) intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt index f027af2c9d4..c75dfdfc990 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt @@ -5,7 +5,6 @@ import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context import android.content.Intent -import android.graphics.Color import android.os.Bundle import android.util.Log import android.util.TypedValue @@ -23,17 +22,17 @@ import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.canSupportPrecision import io.homeassistant.companion.android.common.data.integration.friendlyState import io.homeassistant.companion.android.common.data.integration.onEntityPressedWithoutState -import io.homeassistant.companion.android.database.widget.StaticWidgetDao +import io.homeassistant.companion.android.common.data.repositories.StaticWidgetRepository import io.homeassistant.companion.android.database.widget.StaticWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.database.widget.WidgetTapAction import io.homeassistant.companion.android.util.getAttribute import io.homeassistant.companion.android.widgets.BaseWidgetProvider -import javax.inject.Inject +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @AndroidEntryPoint -class EntityWidget : BaseWidgetProvider() { +class EntityWidget : BaseWidgetProvider>?>() { companion object { private const val TAG = "StaticWidget" @@ -54,14 +53,11 @@ class EntityWidget : BaseWidgetProvider() { private data class ResolvedText(val text: CharSequence?, val exception: Boolean = false) } - @Inject - lateinit var staticWidgetDao: StaticWidgetDao - override fun getWidgetProvider(context: Context): ComponentName = ComponentName(context, EntityWidget::class.java) - override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedEntity: Entity>?): RemoteViews { - val widget = staticWidgetDao.get(appWidgetId) + override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, hasActiveConnection: Boolean, suggestedEntity: Entity>?): RemoteViews { + val widget = repository.get(appWidgetId) val intent = Intent(context, EntityWidget::class.java).apply { action = if (widget?.tapAction == WidgetTapAction.TOGGLE) TOGGLE_ENTITY else UPDATE_VIEW @@ -84,11 +80,12 @@ class EntityWidget : BaseWidgetProvider() { var textColor = context.getAttribute(R.attr.colorWidgetOnBackground, ContextCompat.getColor(context, commonR.color.colorWidgetButtonLabel)) widget.textColor?.let { textColor = it.toColorInt() } - setInt(R.id.widgetLayout, "setBackgroundColor", Color.TRANSPARENT) setTextColor(R.id.widgetText, textColor) setTextColor(R.id.widgetLabel, textColor) } + setWidgetBackground(this, R.id.widgetLayout, widget) + // Content setViewVisibility( R.id.widgetTextLayout, @@ -143,9 +140,6 @@ class EntityWidget : BaseWidgetProvider() { return views } - override suspend fun getAllWidgetIdsWithEntities(context: Context): Map>> = - staticWidgetDao.getAll().associate { it.id to (it.serverId to listOf(it.entityId)) } - private suspend fun resolveTextToShow( context: Context, serverId: Int, @@ -177,11 +171,11 @@ class EntityWidget : BaseWidgetProvider() { null } if (attributeIds == null) { - staticWidgetDao.updateWidgetLastUpdate( + repository.updateWidgetLastUpdate( appWidgetId, - entity?.friendlyState(context, entityOptions) ?: staticWidgetDao.get(appWidgetId)?.lastUpdate ?: "" + entity?.friendlyState(context, entityOptions) ?: repository.get(appWidgetId)?.lastUpdate ?: "" ) - return ResolvedText(staticWidgetDao.get(appWidgetId)?.lastUpdate, entityCaughtException) + return ResolvedText(repository.get(appWidgetId)?.lastUpdate, entityCaughtException) } try { @@ -191,12 +185,12 @@ class EntityWidget : BaseWidgetProvider() { val lastUpdate = entity?.friendlyState(context, entityOptions).plus(if (attributeValues.isNotEmpty()) stateSeparator else "") .plus(attributeValues.joinToString(attributeSeparator)) - staticWidgetDao.updateWidgetLastUpdate(appWidgetId, lastUpdate) + repository.updateWidgetLastUpdate(appWidgetId, lastUpdate) return ResolvedText(lastUpdate) } catch (e: Exception) { Log.e(TAG, "Unable to fetch entity state and attributes", e) } - return ResolvedText(staticWidgetDao.get(appWidgetId)?.lastUpdate, true) + return ResolvedText(repository.get(appWidgetId)?.lastUpdate, true) } override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { @@ -227,7 +221,7 @@ class EntityWidget : BaseWidgetProvider() { "entity id: " + entitySelection + System.lineSeparator() + "attribute: " + (attributeSelection ?: "N/A") ) - staticWidgetDao.add( + repository.add( StaticWidgetEntity( appWidgetId, serverId, @@ -238,20 +232,20 @@ class EntityWidget : BaseWidgetProvider() { stateSeparatorSelection ?: "", attributeSeparatorSelection ?: "", tapActionSelection, - staticWidgetDao.get(appWidgetId)?.lastUpdate ?: "", + repository.get(appWidgetId)?.lastUpdate ?: "", backgroundTypeSelection, textColorSelection ) ) - onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId)) + forceUpdateView(context, appWidgetId) + // onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId)) } } - override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity<*>) { + override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity>?) { widgetScope?.launch { - val views = getWidgetRemoteViews(context, appWidgetId, entity as Entity>) - AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views) + forceUpdateView(context, appWidgetId) } } @@ -265,7 +259,7 @@ class EntityWidget : BaseWidgetProvider() { appWidgetManager.partiallyUpdateAppWidget(appWidgetId, loadingViews) var success = false - staticWidgetDao.get(appWidgetId)?.let { + repository.get(appWidgetId)?.let { try { onEntityPressedWithoutState( it.entityId, @@ -280,8 +274,7 @@ class EntityWidget : BaseWidgetProvider() { if (!success) { Toast.makeText(context, commonR.string.action_failure, Toast.LENGTH_LONG).show() - val views = getWidgetRemoteViews(context, appWidgetId) - appWidgetManager.updateAppWidget(appWidgetId, views) + forceUpdateView(context, appWidgetId) } // else update will be triggered by websocket subscription } } @@ -294,10 +287,5 @@ class EntityWidget : BaseWidgetProvider() { } } - override fun onDeleted(context: Context, appWidgetIds: IntArray) { - widgetScope?.launch { - staticWidgetDao.deleteAll(appWidgetIds) - appWidgetIds.forEach { removeSubscription(it) } - } - } + override suspend fun getUpdates(serverId: Int, entityIds: List): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> } diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidgetConfigureActivity.kt index 32e30e37450..6571963f66a 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidgetConfigureActivity.kt @@ -28,7 +28,7 @@ import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.EntityExt import io.homeassistant.companion.android.common.data.integration.domain import io.homeassistant.companion.android.common.data.integration.friendlyName -import io.homeassistant.companion.android.database.widget.StaticWidgetDao +import io.homeassistant.companion.android.common.data.repositories.StaticWidgetRepository import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.database.widget.WidgetTapAction import io.homeassistant.companion.android.databinding.WidgetStaticConfigureBinding @@ -38,22 +38,17 @@ import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity import io.homeassistant.companion.android.widgets.BaseWidgetProvider import io.homeassistant.companion.android.widgets.common.SingleItemArrayAdapter import io.homeassistant.companion.android.widgets.common.WidgetUtils -import javax.inject.Inject import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @AndroidEntryPoint -class EntityWidgetConfigureActivity : BaseWidgetConfigureActivity() { +class EntityWidgetConfigureActivity : BaseWidgetConfigureActivity() { companion object { private const val TAG: String = "StaticWidgetConfigAct" private const val PIN_WIDGET_CALLBACK = "io.homeassistant.companion.android.widgets.entity.EntityWidgetConfigureActivity.PIN_WIDGET_CALLBACK" } - @Inject - lateinit var staticWidgetDao: StaticWidgetDao - override val dao get() = staticWidgetDao - private var entities = mutableMapOf>>() private var selectedEntity: Entity? = null @@ -127,7 +122,7 @@ class EntityWidgetConfigureActivity : BaseWidgetConfigureActivity() { return } - val staticWidget = staticWidgetDao.get(appWidgetId) + val staticWidget = repository.get(appWidgetId) val tapActionValues = listOf(getString(commonR.string.widget_tap_action_toggle), getString(commonR.string.refresh)) binding.tapActionList.adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, tapActionValues) diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt index 205e5f62fb9..945708ca9d6 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt @@ -5,7 +5,6 @@ import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context import android.content.Intent -import android.graphics.Color import android.os.Bundle import android.os.Handler import android.os.Looper @@ -23,23 +22,20 @@ import io.homeassistant.companion.android.BuildConfig import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.data.integration.Entity -import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetDao +import io.homeassistant.companion.android.common.data.repositories.MediaPlayerControlsWidgetRepository import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetBackgroundType -import io.homeassistant.companion.android.util.hasActiveConnection import io.homeassistant.companion.android.widgets.BaseWidgetProvider import java.util.LinkedList -import javax.inject.Inject import kotlin.collections.HashMap +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @AndroidEntryPoint -class MediaPlayerControlsWidget : BaseWidgetProvider() { +class MediaPlayerControlsWidget : BaseWidgetProvider>>() { companion object { private const val TAG = "MediaPlayCtrlsWidget" - internal const val RECEIVE_DATA = - "io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.RECEIVE_DATA" internal const val UPDATE_MEDIA_IMAGE = "io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.UPDATE_MEDIA_IMAGE" internal const val CALL_PREV_TRACK = @@ -67,44 +63,10 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { internal const val EXTRA_BACKGROUND_TYPE = "EXTRA_BACKGROUND_TYPE" } - @Inject - lateinit var mediaPlayCtrlWidgetDao: MediaPlayerControlsWidgetDao - override fun getWidgetProvider(context: Context): ComponentName = ComponentName(context, MediaPlayerControlsWidget::class.java) - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { - // There may be multiple widgets active, so update all of them - appWidgetIds.forEach { appWidgetId -> - updateView( - context, - appWidgetId, - appWidgetManager - ) - } - } - - private fun updateView( - context: Context, - appWidgetId: Int, - appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context) - ) { - if (!context.hasActiveConnection()) { - Log.d(TAG, "Skipping widget update since network connection is not active") - return - } - widgetScope?.launch { - val views = getWidgetRemoteViews(context, appWidgetId) - appWidgetManager.updateAppWidget(appWidgetId, views) - onScreenOn(context) - } - } - - override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedEntity: Entity>?): RemoteViews { + override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, hasActiveConnection: Boolean, suggestedEntity: Entity>?): RemoteViews { val updateMediaIntent = Intent(context, MediaPlayerControlsWidget::class.java).apply { action = UPDATE_MEDIA_IMAGE putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) @@ -145,7 +107,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) } - val widget = mediaPlayCtrlWidgetDao.get(appWidgetId) + val widget = repository.get(appWidgetId) val useDynamicColors = widget?.backgroundType == WidgetBackgroundType.DYNAMICCOLOR && DynamicColors.isDynamicColorAvailable() return RemoteViews(context.packageName, if (useDynamicColors) R.layout.widget_media_controls_wrapper_dynamiccolor else R.layout.widget_media_controls_wrapper_default).apply { if (widget != null) { @@ -176,9 +138,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { val album = entity?.attributes?.get("media_album_name")?.toString() val icon = entity?.attributes?.get("icon")?.toString() - if (widget.backgroundType == WidgetBackgroundType.TRANSPARENT) { - setInt(R.id.widgetLayout, "setBackgroundColor", Color.TRANSPARENT) - } + setWidgetBackground(this, R.id.widgetLayout, widget) if ((artist != null || album != null) && title != null) { setTextViewText( @@ -408,66 +368,6 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { } } - override suspend fun getAllWidgetIdsWithEntities(context: Context): Map>> = - mediaPlayCtrlWidgetDao.getAll().associate { it.id to (it.serverId to it.entityId.split(",")) } - - private suspend fun getEntity(context: Context, serverId: Int, entityIds: List, suggestedEntity: Entity>?): Entity>? { - val entity: Entity>? - try { - entity = if (suggestedEntity != null && entityIds.contains(suggestedEntity.entityId)) { - suggestedEntity - } else { - val entities: LinkedList>?> = LinkedList() - entityIds.forEach { - val e = serverManager.integrationRepository(serverId).getEntity(it) - if (e?.state == "playing") return e - entities.add(e) - } - return entities[0] - } - } catch (e: Exception) { - Log.d(TAG, "Failed to fetch entity or entity does not exist") - if (lastIntent == UPDATE_MEDIA_IMAGE) { - Toast.makeText(context, commonR.string.widget_entity_fetch_error, Toast.LENGTH_LONG).show() - } - return null - } - - return entity - } - - override fun onReceive(context: Context, intent: Intent) { - lastIntent = intent.action.toString() - val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) - - Log.d( - TAG, - "Broadcast received: " + System.lineSeparator() + - "Broadcast action: " + lastIntent + System.lineSeparator() + - "AppWidgetId: " + appWidgetId - ) - - super.onReceive(context, intent) - when (lastIntent) { - UPDATE_VIEW -> updateView( - context, - appWidgetId - ) - RECEIVE_DATA -> { - saveEntityConfiguration(context, intent.extras, appWidgetId) - super.onScreenOn(context) - } - UPDATE_MEDIA_IMAGE -> updateView(context, appWidgetId) - CALL_PREV_TRACK -> callPreviousTrackAction(context, appWidgetId) - CALL_REWIND -> callRewindAction(context, appWidgetId) - CALL_PLAYPAUSE -> callPlayPauseAction(context, appWidgetId) - CALL_FASTFORWARD -> callFastForwardAction(context, appWidgetId) - CALL_NEXT_TRACK -> callNextTrackAction(context, appWidgetId) - CALL_VOLUME_DOWN -> callVolumeDownAction(context, appWidgetId) - CALL_VOLUME_UP -> callVolumeUpAction(context, appWidgetId) - } - } - override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { if (extras == null) return @@ -492,7 +392,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { "Saving action call config data:" + System.lineSeparator() + "entity id: " + entitySelection + System.lineSeparator() ) - mediaPlayCtrlWidgetDao.add( + repository.add( MediaPlayerControlsWidgetEntity( appWidgetId, serverId, @@ -506,23 +406,65 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { ) ) - onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId)) + forceUpdateView(context, appWidgetId) } } - override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity<*>) { - mediaPlayCtrlWidgetDao.get(appWidgetId)?.let { + override fun onReceive(context: Context, intent: Intent) { + val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + super.onReceive(context, intent) + when (lastIntent) { + UPDATE_MEDIA_IMAGE -> forceUpdateView(context, appWidgetId) + CALL_PREV_TRACK -> callPreviousTrackAction(context, appWidgetId) + CALL_REWIND -> callRewindAction(context, appWidgetId) + CALL_PLAYPAUSE -> callPlayPauseAction(context, appWidgetId) + CALL_FASTFORWARD -> callFastForwardAction(context, appWidgetId) + CALL_NEXT_TRACK -> callNextTrackAction(context, appWidgetId) + CALL_VOLUME_DOWN -> callVolumeDownAction(context, appWidgetId) + CALL_VOLUME_UP -> callVolumeUpAction(context, appWidgetId) + } + } + + override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity>) { + super.onEntityStateChanged(context, appWidgetId, entity) + repository.get(appWidgetId)?.let { widgetScope?.launch { - val views = getWidgetRemoteViews(context, appWidgetId, getEntity(context, it.serverId, it.entityId.split(","), null)) - AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views) + forceUpdateView(context, appWidgetId) + } + } + } + + override suspend fun getUpdates(serverId: Int, entityIds: List): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> + + private suspend fun getEntity(context: Context, serverId: Int, entityIds: List, suggestedEntity: Entity>?): Entity>? { + val entity: Entity>? + try { + entity = if (suggestedEntity != null && entityIds.contains(suggestedEntity.entityId)) { + suggestedEntity + } else { + val entities: LinkedList>?> = LinkedList() + entityIds.forEach { + val e = serverManager.integrationRepository(serverId).getEntity(it) + if (e?.state == "playing") return e + entities.add(e) + } + return entities[0] } + } catch (e: Exception) { + Log.d(TAG, "Failed to fetch entity or entity does not exist") + if (lastIntent == UPDATE_MEDIA_IMAGE) { + Toast.makeText(context, commonR.string.widget_entity_fetch_error, Toast.LENGTH_LONG).show() + } + return null } + + return entity } private fun callPreviousTrackAction(context: Context, appWidgetId: Int) { widgetScope?.launch { Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId") - val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId) + val entity: MediaPlayerControlsWidgetEntity? = repository.get(appWidgetId) if (entity == null) { Log.d(TAG, "Failed to retrieve media player entity") @@ -548,7 +490,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { private fun callRewindAction(context: Context, appWidgetId: Int) { widgetScope?.launch { Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId") - val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId) + val entity: MediaPlayerControlsWidgetEntity? = repository.get(appWidgetId) if (entity == null) { Log.d(TAG, "Failed to retrieve media player entity") @@ -602,7 +544,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { private fun callPlayPauseAction(context: Context, appWidgetId: Int) { widgetScope?.launch { Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId") - val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId) + val entity: MediaPlayerControlsWidgetEntity? = repository.get(appWidgetId) if (entity == null) { Log.d(TAG, "Failed to retrieve media player entity") @@ -632,7 +574,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { private fun callFastForwardAction(context: Context, appWidgetId: Int) { widgetScope?.launch { Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId") - val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId) + val entity: MediaPlayerControlsWidgetEntity? = repository.get(appWidgetId) if (entity == null) { Log.d(TAG, "Failed to retrieve media player entity") @@ -686,7 +628,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { private fun callNextTrackAction(context: Context, appWidgetId: Int) { widgetScope?.launch { Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId") - val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId) + val entity: MediaPlayerControlsWidgetEntity? = repository.get(appWidgetId) if (entity == null) { Log.d(TAG, "Failed to retrieve media player entity") @@ -716,7 +658,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { private fun callVolumeDownAction(context: Context, appWidgetId: Int) { widgetScope?.launch { Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId") - val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId) + val entity: MediaPlayerControlsWidgetEntity? = repository.get(appWidgetId) if (entity == null) { Log.d(TAG, "Failed to retrieve media player entity") @@ -746,7 +688,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { private fun callVolumeUpAction(context: Context, appWidgetId: Int) { widgetScope?.launch { Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId") - val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId) + val entity: MediaPlayerControlsWidgetEntity? = repository.get(appWidgetId) if (entity == null) { Log.d(TAG, "Failed to retrieve media player entity") @@ -772,11 +714,4 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() { } } } - - override fun onDeleted(context: Context, appWidgetIds: IntArray) { - widgetScope?.launch { - mediaPlayCtrlWidgetDao.deleteAll(appWidgetIds) - appWidgetIds.forEach { removeSubscription(it) } - } - } } diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidgetConfigureActivity.kt index fafa56fb5c5..22abb1c5c77 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidgetConfigureActivity.kt @@ -19,20 +19,20 @@ import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.domain -import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetDao +import io.homeassistant.companion.android.common.data.repositories.MediaPlayerControlsWidgetRepository import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.databinding.WidgetMediaControlsConfigureBinding import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity +import io.homeassistant.companion.android.widgets.BaseWidgetProvider.Companion.RECEIVE_DATA import io.homeassistant.companion.android.widgets.common.SingleItemArrayAdapter import io.homeassistant.companion.android.widgets.common.WidgetUtils import java.util.LinkedList -import javax.inject.Inject import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @AndroidEntryPoint -class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity() { +class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity() { companion object { private const val TAG: String = "MediaWidgetConfigAct" @@ -41,10 +41,6 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity() private var requestLauncherSetup = false - @Inject - lateinit var mediaPlayerControlsWidgetDao: MediaPlayerControlsWidgetDao - override val dao get() = mediaPlayerControlsWidgetDao - private lateinit var binding: WidgetMediaControlsConfigureBinding override val serverSelect: View @@ -115,7 +111,7 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity() return } - val mediaPlayerWidget = mediaPlayerControlsWidgetDao.get(appWidgetId) + val mediaPlayerWidget = repository.get(appWidgetId) val backgroundTypeValues = WidgetUtils.getBackgroundOptionList(this) binding.backgroundType.adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, backgroundTypeValues) @@ -207,7 +203,7 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity() // Set up a broadcast intent and pass the service call data as extras val intent = Intent() - intent.action = MediaPlayerControlsWidget.RECEIVE_DATA + intent.action = RECEIVE_DATA intent.component = ComponentName(context, MediaPlayerControlsWidget::class.java) intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidget.kt index 528a74023c4..b62609a1540 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidget.kt @@ -2,12 +2,9 @@ package io.homeassistant.companion.android.widgets.template import android.app.PendingIntent import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider 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.util.TypedValue @@ -21,189 +18,31 @@ import com.google.android.material.color.DynamicColors import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.R as commonR -import io.homeassistant.companion.android.common.data.servers.ServerManager -import io.homeassistant.companion.android.database.widget.TemplateWidgetDao +import io.homeassistant.companion.android.common.data.repositories.TemplateWidgetRepository import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.util.getAttribute -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.isActive +import io.homeassistant.companion.android.widgets.BaseWidgetProvider +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @AndroidEntryPoint -class TemplateWidget : AppWidgetProvider() { +class TemplateWidget : BaseWidgetProvider() { + companion object { private const val TAG = "TemplateWidget" - const val UPDATE_VIEW = - "io.homeassistant.companion.android.widgets.template.TemplateWidget.UPDATE_VIEW" - const val RECEIVE_DATA = - "io.homeassistant.companion.android.widgets.template.TemplateWidget.RECEIVE_DATA" - internal const val EXTRA_SERVER_ID = "EXTRA_SERVER_ID" internal const val EXTRA_TEMPLATE = "extra_template" internal const val EXTRA_TEXT_SIZE = "EXTRA_TEXT_SIZE" internal const val EXTRA_BACKGROUND_TYPE = "EXTRA_BACKGROUND_TYPE" internal const val EXTRA_TEXT_COLOR = "EXTRA_TEXT_COLOR" - - private var widgetScope: CoroutineScope? = null - private val widgetTemplates = mutableMapOf() - private val widgetJobs = mutableMapOf() - } - - @Inject - lateinit var serverManager: ServerManager - - @Inject - lateinit var templateWidgetDao: TemplateWidgetDao - - private var thisSetScope = false - private var lastIntent = "" - - init { - setupWidgetScope() - } - - private fun setupWidgetScope() { - if (widgetScope == null || !widgetScope!!.isActive) { - widgetScope = CoroutineScope(Dispatchers.Main + Job()) - thisSetScope = true - } - } - - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { - // 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) - } - } - } - - override fun onDeleted(context: Context, appWidgetIds: IntArray) { - // When the user deletes the widget, delete the preference associated with it. - widgetScope?.launch { - templateWidgetDao.deleteAll(appWidgetIds) - appWidgetIds.forEach { - widgetTemplates.remove(it) - widgetJobs[it]?.cancel() - widgetJobs.remove(it) - } - } - } - - override fun onReceive(context: Context, intent: Intent) { - lastIntent = intent.action.toString() - val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) - - 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() - } - } - - private fun onScreenOn(context: Context) { - setupWidgetScope() - if (!serverManager.isRegistered()) return - widgetScope!!.launch { - updateAllWidgets(context) - - val allWidgets = templateWidgetDao.getAll() - val widgetsWithDifferentTemplate = allWidgets.filter { it.template != widgetTemplates[it.id] } - if (widgetsWithDifferentTemplate.isNotEmpty()) { - if (thisSetScope) { - ContextCompat.registerReceiver( - context.applicationContext, - this@TemplateWidget, - IntentFilter(Intent.ACTION_SCREEN_OFF), - ContextCompat.RECEIVER_NOT_EXPORTED - ) - } - - widgetsWithDifferentTemplate.forEach { widget -> - widgetJobs[widget.id]?.cancel() - - val templateUpdates = - if (serverManager.getServer(widget.serverId) != null) { - serverManager.integrationRepository(widget.serverId).getTemplateUpdates(widget.template) - } else { - null - } - if (templateUpdates != null) { - widgetTemplates[widget.id] = widget.template - widgetJobs[widget.id] = widgetScope!!.launch { - templateUpdates.collect { - onTemplateChanged(context, widget.id, it) - } - } - } else { // Remove data to make it retry on the next update - widgetTemplates.remove(widget.id) - widgetJobs.remove(widget.id) - } - } - } - } } - private fun onScreenOff() { - if (thisSetScope) { - widgetScope?.cancel() - thisSetScope = false - widgetTemplates.clear() - widgetJobs.clear() - } - } + override fun getWidgetProvider(context: Context): ComponentName = + ComponentName(context, TemplateWidget::class.java) - private suspend fun updateAllWidgets( - context: Context - ) { - val systemWidgetIds = AppWidgetManager.getInstance(context) - .getAppWidgetIds(ComponentName(context, TemplateWidget::class.java)) - .toSet() - val dbWidgetIds = templateWidgetDao.getAll().map { it.id } - - val invalidWidgetIds = dbWidgetIds.minus(systemWidgetIds) - if (invalidWidgetIds.isNotEmpty()) { - Log.i(TAG, "Found widgets $invalidWidgetIds in database, but not in AppWidgetManager - sending onDeleted") - onDeleted(context, invalidWidgetIds.toIntArray()) - } - - dbWidgetIds.filter { systemWidgetIds.contains(it) }.forEach { - updateView(context, it) - } - } - - private fun updateView( - context: Context, - appWidgetId: Int, - appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context) - ) { - widgetScope?.launch { - val views = getWidgetRemoteViews(context, appWidgetId) - appWidgetManager.updateAppWidget(appWidgetId, views) - } - } - - private suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedTemplate: String? = null): RemoteViews { + override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, hasActiveConnection: Boolean, suggestedEntity: String?): RemoteViews { // Every time AppWidgetManager.updateAppWidget(...) is called, the button listener // and label need to be re-assigned, or the next time the layout updates // (e.g home screen rotation) the widget will fall back on its default layout @@ -214,7 +53,7 @@ class TemplateWidget : AppWidgetProvider() { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) } - val widget = templateWidgetDao.get(appWidgetId) + val widget = repository.get(appWidgetId) val useDynamicColors = widget?.backgroundType == WidgetBackgroundType.DYNAMICCOLOR && DynamicColors.isDynamicColorAvailable() return RemoteViews(context.packageName, if (useDynamicColors) R.layout.widget_template_wrapper_dynamiccolor else R.layout.widget_template_wrapper_default).apply { @@ -233,15 +72,16 @@ class TemplateWidget : AppWidgetProvider() { var textColor = context.getAttribute(R.attr.colorWidgetOnBackground, ContextCompat.getColor(context, commonR.color.colorWidgetButtonLabel)) widget.textColor?.let { textColor = it.toColorInt() } - setInt(R.id.widgetLayout, "setBackgroundColor", Color.TRANSPARENT) setTextColor(R.id.widgetTemplateText, textColor) } + setWidgetBackground(this, R.id.widgetLayout, widget) + // Content - var renderedTemplate: String? = templateWidgetDao.get(appWidgetId)?.lastUpdate ?: context.getString(commonR.string.loading) + var renderedTemplate: String? = repository.get(appWidgetId)?.lastUpdate ?: context.getString(commonR.string.loading) try { - renderedTemplate = suggestedTemplate ?: serverManager.integrationRepository(widget.serverId).renderTemplate(widget.template, mapOf()).toString() - templateWidgetDao.updateTemplateWidgetLastUpdate( + renderedTemplate = suggestedEntity ?: serverManager.integrationRepository(widget.serverId).renderTemplate(widget.template, mapOf()).toString() + repository.updateWidgetLastUpdate( appWidgetId, renderedTemplate ) @@ -265,7 +105,19 @@ class TemplateWidget : AppWidgetProvider() { } } - private fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { + override fun onWidgetsViewUpdated(context: Context, appWidgetId: Int, appWidgetManager: AppWidgetManager, remoteViews: RemoteViews, hasActiveConnection: Boolean) { + super.onWidgetsViewUpdated(context, appWidgetId, appWidgetManager, remoteViews, hasActiveConnection) + remoteViews.setViewVisibility( + R.id.widgetTemplateError, + if (!hasActiveConnection) View.VISIBLE else View.GONE + ) + } + + override suspend fun getUpdates(serverId: Int, entityIds: List): Flow? { + return serverManager.integrationRepository(serverId).getTemplateUpdates(entityIds.first()) + } + + override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { if (extras == null) return val serverId = if (extras.containsKey(EXTRA_SERVER_ID)) extras.getInt(EXTRA_SERVER_ID) else null @@ -281,25 +133,18 @@ class TemplateWidget : AppWidgetProvider() { } widgetScope?.launch { - templateWidgetDao.add( + repository.add( TemplateWidgetEntity( appWidgetId, serverId, + "template_$appWidgetId", template, textSize, - templateWidgetDao.get(appWidgetId)?.lastUpdate ?: "Loading", + repository.get(appWidgetId)?.lastUpdate ?: ContextCompat.getString(context, commonR.string.loading), backgroundTypeSelection, textColorSelection ) ) - onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId)) - } - } - - private fun onTemplateChanged(context: Context, appWidgetId: Int, template: String?) { - widgetScope?.launch { - val views = getWidgetRemoteViews(context, appWidgetId, template) - AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views) } } } diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt index 43b108ee005..7f6041001ed 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt @@ -21,29 +21,25 @@ import androidx.lifecycle.lifecycleScope import com.fasterxml.jackson.databind.JsonMappingException import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.common.R as commonR -import io.homeassistant.companion.android.database.widget.TemplateWidgetDao +import io.homeassistant.companion.android.common.data.repositories.TemplateWidgetRepository import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.databinding.WidgetTemplateConfigureBinding import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel import io.homeassistant.companion.android.util.getHexForColor import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity +import io.homeassistant.companion.android.widgets.BaseWidgetProvider.Companion.RECEIVE_DATA import io.homeassistant.companion.android.widgets.common.WidgetUtils -import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @AndroidEntryPoint -class TemplateWidgetConfigureActivity : BaseWidgetConfigureActivity() { +class TemplateWidgetConfigureActivity : BaseWidgetConfigureActivity() { companion object { private const val TAG: String = "TemplateWidgetConfigAct" private const val PIN_WIDGET_CALLBACK = "io.homeassistant.companion.android.widgets.template.TemplateWidgetConfigureActivity.PIN_WIDGET_CALLBACK" } - @Inject - lateinit var templateWidgetDao: TemplateWidgetDao - override val dao get() = templateWidgetDao - private lateinit var binding: WidgetTemplateConfigureBinding override val serverSelect: View @@ -84,7 +80,7 @@ class TemplateWidgetConfigureActivity : BaseWidgetConfigureActivity() { return } - val templateWidget = templateWidgetDao.get(appWidgetId) + val templateWidget = repository.get(appWidgetId) val backgroundTypeValues = WidgetUtils.getBackgroundOptionList(this) binding.backgroundType.adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, backgroundTypeValues) @@ -171,7 +167,7 @@ class TemplateWidgetConfigureActivity : BaseWidgetConfigureActivity() { } val createIntent = Intent().apply { - action = TemplateWidget.RECEIVE_DATA + action = RECEIVE_DATA component = ComponentName(applicationContext, TemplateWidget::class.java) putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) putExtra(TemplateWidget.EXTRA_SERVER_ID, selectedServerId) diff --git a/app/src/main/res/layout/widget_camera.xml b/app/src/main/res/layout/widget_camera.xml index acc4fe3dc50..435e03f8842 100755 --- a/app/src/main/res/layout/widget_camera.xml +++ b/app/src/main/res/layout/widget_camera.xml @@ -3,36 +3,47 @@ android:id="@+id/widgetLayout" android:layout_width="match_parent" android:layout_height="match_parent" - android:minHeight="40dp" + android:background="@drawable/widget_button_background" android:minWidth="40dp" - android:background="@drawable/widget_transparent_background" + android:minHeight="40dp" android:theme="@style/Theme.HomeAssistant.Widget"> - + + + android:background="@drawable/widget_button_background" + android:layout_height="match_parent"> + + + + + android:visibility="gone" /> \ No newline at end of file diff --git a/app/src/main/res/layout/widget_media_controls.xml b/app/src/main/res/layout/widget_media_controls.xml index 5e90b7d0122..52ef5764e76 100644 --- a/app/src/main/res/layout/widget_media_controls.xml +++ b/app/src/main/res/layout/widget_media_controls.xml @@ -4,10 +4,10 @@ android:id="@+id/widgetLayout" android:layout_width="match_parent" android:layout_height="match_parent" - android:minHeight="40dp" - android:minWidth="40dp" android:background="@drawable/widget_button_background" android:clipToOutline="true" + android:minWidth="40dp" + android:minHeight="40dp" tools:ignore="UseAppTint"> + android:layout_width="100dp" + android:layout_height="100dp" + android:layout_gravity="center"> + android:src="@android:drawable/ic_media_play" /> + android:tint="?colorWidgetButton" + android:visibility="gone" /> + android:tint="?colorWidgetButton" + android:visibility="gone" /> - + - + @@ -145,7 +145,7 @@ android:exported="false"> - + - + @@ -182,7 +182,7 @@ - + { + + fun get(id: Int): T? + + fun exist(appWidgetId: Int): Boolean { + return get(appWidgetId) != null + } + + suspend fun add(entity: T) + + suspend fun delete(id: Int) + + suspend fun deleteAll(ids: IntArray) + + suspend fun getAll(): List + + suspend fun getAllFlow(): Flow> + + suspend fun updateWidgetLastUpdate(widgetId: Int, lastUpdate: String) +} diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepository.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepository.kt new file mode 100644 index 00000000000..481fcb9ed6c --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepository.kt @@ -0,0 +1,5 @@ +package io.homeassistant.companion.android.common.data.repositories + +import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity + +interface ButtonWidgetRepository : BaseDaoWidgetRepository diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepositoryImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepositoryImpl.kt new file mode 100644 index 00000000000..3266449da62 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepositoryImpl.kt @@ -0,0 +1,29 @@ +package io.homeassistant.companion.android.common.data.repositories + +import io.homeassistant.companion.android.database.widget.ButtonWidgetDao +import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +class ButtonWidgetRepositoryImpl @Inject constructor( + private val dao: ButtonWidgetDao +) : ButtonWidgetRepository { + + override fun get(id: Int): ButtonWidgetEntity? { + return dao.get(id) + } + + override suspend fun add(entity: ButtonWidgetEntity) = dao.add(entity) + + override suspend fun delete(id: Int) = dao.delete(id) + + override suspend fun deleteAll(ids: IntArray) = dao.deleteAll(ids) + + override suspend fun getAll(): List = dao.getAll() + + override suspend fun getAllFlow(): Flow> = dao.getAllFlow().flowOn(Dispatchers.IO) + + override suspend fun updateWidgetLastUpdate(widgetId: Int, lastUpdate: String) = error("Not Implemented") +} diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepository.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepository.kt new file mode 100644 index 00000000000..f712fc2632f --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepository.kt @@ -0,0 +1,5 @@ +package io.homeassistant.companion.android.common.data.repositories + +import io.homeassistant.companion.android.database.widget.CameraWidgetEntity + +interface CameraWidgetRepository : BaseDaoWidgetRepository diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepositoryImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepositoryImpl.kt new file mode 100644 index 00000000000..a70f37c1514 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepositoryImpl.kt @@ -0,0 +1,25 @@ +package io.homeassistant.companion.android.common.data.repositories + +import io.homeassistant.companion.android.database.widget.CameraWidgetDao +import io.homeassistant.companion.android.database.widget.CameraWidgetEntity +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +class CameraWidgetRepositoryImpl @Inject constructor( + private val dao: CameraWidgetDao +) : CameraWidgetRepository { + + override suspend fun add(entity: CameraWidgetEntity) = dao.add(entity) + + override suspend fun delete(id: Int) = dao.delete(id) + + override suspend fun deleteAll(ids: IntArray) = dao.deleteAll(ids) + + override suspend fun getAll(): List = dao.getAll() + + override suspend fun getAllFlow(): Flow> = dao.getAllFlow() + + override suspend fun updateWidgetLastUpdate(widgetId: Int, lastUpdate: String) = error("Not Implemented") + + override fun get(id: Int): CameraWidgetEntity? = dao.get(id) +} diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepository.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepository.kt new file mode 100644 index 00000000000..090525a3f19 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepository.kt @@ -0,0 +1,5 @@ +package io.homeassistant.companion.android.common.data.repositories + +import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetEntity + +interface MediaPlayerControlsWidgetRepository : BaseDaoWidgetRepository diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepositoryImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepositoryImpl.kt new file mode 100644 index 00000000000..24058733b97 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepositoryImpl.kt @@ -0,0 +1,27 @@ +package io.homeassistant.companion.android.common.data.repositories + +import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetDao +import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetEntity +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +class MediaPlayerControlsWidgetRepositoryImpl @Inject constructor( + private val dao: MediaPlayerControlsWidgetDao +) : MediaPlayerControlsWidgetRepository { + + override fun get(id: Int): MediaPlayerControlsWidgetEntity? = dao.get(id) + + override suspend fun add(entity: MediaPlayerControlsWidgetEntity) = dao.add(entity) + + override suspend fun delete(id: Int) = dao.delete(id) + + override suspend fun deleteAll(ids: IntArray) = dao.deleteAll(ids) + + override suspend fun getAll(): List = dao.getAll() + + override suspend fun getAllFlow(): Flow> = dao.getAllFlow().flowOn(Dispatchers.IO) + + override suspend fun updateWidgetLastUpdate(widgetId: Int, lastUpdate: String) = error("Not Implemented") +} diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/RepositoryModule.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/RepositoryModule.kt new file mode 100644 index 00000000000..be6574a07bd --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/RepositoryModule.kt @@ -0,0 +1,36 @@ +package io.homeassistant.companion.android.common.data.repositories + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + + @Binds + abstract fun bindMediaPlayerWidgetRepository( + impl: MediaPlayerControlsWidgetRepositoryImpl + ): MediaPlayerControlsWidgetRepository + + @Binds + abstract fun bindTemplateWidgetRepository( + impl: TemplateWidgetRepositoryImpl + ): TemplateWidgetRepository + + @Binds + abstract fun bindEntityWidgetRepository( + impl: StaticWidgetRepositoryImpl + ): StaticWidgetRepository + + @Binds + abstract fun bindCameraWidgetRepository( + impl: CameraWidgetRepositoryImpl + ): CameraWidgetRepository + + @Binds + abstract fun bindButtonWidgetRepository( + impl: ButtonWidgetRepositoryImpl + ): ButtonWidgetRepository +} diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepository.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepository.kt new file mode 100644 index 00000000000..eb79b52d779 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepository.kt @@ -0,0 +1,5 @@ +package io.homeassistant.companion.android.common.data.repositories + +import io.homeassistant.companion.android.database.widget.StaticWidgetEntity + +interface StaticWidgetRepository : BaseDaoWidgetRepository diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepositoryImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepositoryImpl.kt new file mode 100644 index 00000000000..ddbc5f03cc4 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepositoryImpl.kt @@ -0,0 +1,27 @@ +package io.homeassistant.companion.android.common.data.repositories + +import io.homeassistant.companion.android.database.widget.StaticWidgetDao +import io.homeassistant.companion.android.database.widget.StaticWidgetEntity +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +class StaticWidgetRepositoryImpl @Inject constructor( + private val dao: StaticWidgetDao +) : StaticWidgetRepository { + + override fun get(id: Int): StaticWidgetEntity? = dao.get(id) + + override suspend fun updateWidgetLastUpdate(widgetId: Int, lastUpdate: String) = dao.updateWidgetLastUpdate(widgetId, lastUpdate) + + override suspend fun add(entity: StaticWidgetEntity) = dao.add(entity) + + override suspend fun delete(id: Int) = dao.delete(id) + + override suspend fun deleteAll(ids: IntArray) = dao.deleteAll(ids) + + override suspend fun getAll(): List = dao.getAll() + + override suspend fun getAllFlow(): Flow> = dao.getAllFlow().flowOn(Dispatchers.IO) +} diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepository.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepository.kt new file mode 100644 index 00000000000..e196c4ceb8c --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepository.kt @@ -0,0 +1,5 @@ +package io.homeassistant.companion.android.common.data.repositories + +import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity + +interface TemplateWidgetRepository : BaseDaoWidgetRepository diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepositoryImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepositoryImpl.kt new file mode 100644 index 00000000000..c04bee81a80 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepositoryImpl.kt @@ -0,0 +1,27 @@ +package io.homeassistant.companion.android.common.data.repositories + +import io.homeassistant.companion.android.database.widget.TemplateWidgetDao +import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +class TemplateWidgetRepositoryImpl @Inject constructor( + private val dao: TemplateWidgetDao +) : TemplateWidgetRepository { + + override fun get(id: Int): TemplateWidgetEntity? = dao.get(id) + + override suspend fun delete(id: Int) = dao.delete(id) + + override suspend fun deleteAll(ids: IntArray) = dao.deleteAll(ids) + + override suspend fun getAll(): List = dao.getAll() + + override suspend fun getAllFlow(): Flow> = dao.getAllFlow().flowOn(Dispatchers.IO) + + override suspend fun updateWidgetLastUpdate(widgetId: Int, lastUpdate: String) = dao.updateTemplateWidgetLastUpdate(widgetId, lastUpdate) + + override suspend fun add(entity: TemplateWidgetEntity) = dao.add(entity) +} diff --git a/common/src/main/java/io/homeassistant/companion/android/database/widget/ButtonWidgetEntity.kt b/common/src/main/java/io/homeassistant/companion/android/database/widget/ButtonWidgetEntity.kt index 3e7d33fb139..e9d7fde7967 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/widget/ButtonWidgetEntity.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/widget/ButtonWidgetEntity.kt @@ -10,6 +10,8 @@ data class ButtonWidgetEntity( override val id: Int, @ColumnInfo(name = "server_id", defaultValue = "0") override val serverId: Int, + @ColumnInfo(name = "entity_id") + override val entityId: String?, @ColumnInfo(name = "icon_name") val iconName: String, @ColumnInfo(name = "domain") diff --git a/common/src/main/java/io/homeassistant/companion/android/database/widget/CameraWidgetEntity.kt b/common/src/main/java/io/homeassistant/companion/android/database/widget/CameraWidgetEntity.kt index 932f6b841b7..e74c393e3ff 100755 --- a/common/src/main/java/io/homeassistant/companion/android/database/widget/CameraWidgetEntity.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/widget/CameraWidgetEntity.kt @@ -11,7 +11,7 @@ data class CameraWidgetEntity( @ColumnInfo(name = "server_id", defaultValue = "0") override val serverId: Int, @ColumnInfo(name = "entity_id") - val entityId: String, + override val entityId: String, @ColumnInfo(name = "tap_action", defaultValue = "REFRESH") val tapAction: WidgetTapAction ) : WidgetEntity diff --git a/common/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetEntity.kt b/common/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetEntity.kt index a421ed59ebc..f7f599059af 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetEntity.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetEntity.kt @@ -11,7 +11,7 @@ data class MediaPlayerControlsWidgetEntity( @ColumnInfo(name = "server_id", defaultValue = "0") override val serverId: Int, @ColumnInfo(name = "entity_id") - val entityId: String, + override val entityId: String, @ColumnInfo(name = "label") val label: String?, @ColumnInfo(name = "show_skip") diff --git a/common/src/main/java/io/homeassistant/companion/android/database/widget/StaticWidgetEntity.kt b/common/src/main/java/io/homeassistant/companion/android/database/widget/StaticWidgetEntity.kt index 7aa044f8634..be0e676a49c 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/widget/StaticWidgetEntity.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/widget/StaticWidgetEntity.kt @@ -11,7 +11,7 @@ data class StaticWidgetEntity( @ColumnInfo(name = "server_id", defaultValue = "0") override val serverId: Int, @ColumnInfo(name = "entity_id") - val entityId: String, + override val entityId: String, @ColumnInfo(name = "attribute_ids") val attributeIds: String?, @ColumnInfo(name = "label") diff --git a/common/src/main/java/io/homeassistant/companion/android/database/widget/TemplateWidgetEntity.kt b/common/src/main/java/io/homeassistant/companion/android/database/widget/TemplateWidgetEntity.kt index 097f10efe80..4ccd58c05fb 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/widget/TemplateWidgetEntity.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/widget/TemplateWidgetEntity.kt @@ -10,6 +10,8 @@ data class TemplateWidgetEntity( override val id: Int, @ColumnInfo(name = "server_id", defaultValue = "0") override val serverId: Int, + @ColumnInfo(name = "entity_id", defaultValue = "template") + override val entityId: String, @ColumnInfo(name = "template") val template: String, @ColumnInfo(name = "text_size", defaultValue = "12.0") diff --git a/common/src/main/java/io/homeassistant/companion/android/database/widget/WidgetEntity.kt b/common/src/main/java/io/homeassistant/companion/android/database/widget/WidgetEntity.kt index dafd23fe02a..d4cea678574 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/widget/WidgetEntity.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/widget/WidgetEntity.kt @@ -3,4 +3,5 @@ package io.homeassistant.companion.android.database.widget interface WidgetEntity { val id: Int val serverId: Int + val entityId: String? } diff --git a/common/src/main/java/io/homeassistant/companion/android/util/DisplayUtils.kt b/common/src/main/java/io/homeassistant/companion/android/util/DisplayUtils.kt new file mode 100644 index 00000000000..39ae696ec48 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/util/DisplayUtils.kt @@ -0,0 +1,10 @@ +package io.homeassistant.companion.android.util + +import android.content.res.Resources + +object DisplayUtils { + + fun getScreenWidth(): Int { + return Resources.getSystem().displayMetrics.widthPixels + } +} From 27a4997b6bc4ce50d2c18ba9e54bca09009569fc Mon Sep 17 00:00:00 2001 From: Ivor Smorenburg Date: Sun, 15 Sep 2024 16:06:22 +0100 Subject: [PATCH 2/9] Update Database Version --- .../48.json | 1140 +++++++++++++++++ .../companion/android/database/AppDatabase.kt | 5 +- 2 files changed, 1143 insertions(+), 2 deletions(-) create mode 100644 common/schemas/io.homeassistant.companion.android.database.AppDatabase/48.json diff --git a/common/schemas/io.homeassistant.companion.android.database.AppDatabase/48.json b/common/schemas/io.homeassistant.companion.android.database.AppDatabase/48.json new file mode 100644 index 00000000000..f960a04a82b --- /dev/null +++ b/common/schemas/io.homeassistant.companion.android.database.AppDatabase/48.json @@ -0,0 +1,1140 @@ +{ + "formatVersion": 1, + "database": { + "version": 48, + "identityHash": "0c9247ff03d0dd1f6d7ef42c18c369ce", + "entities": [ + { + "tableName": "sensor_attributes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))", + "fields": [ + { + "fieldPath": "sensorId", + "columnName": "sensor_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueType", + "columnName": "value_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sensor_id", + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "authentication_list", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`host`))", + "fields": [ + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "host" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sensors", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL, `registered` INTEGER DEFAULT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT DEFAULT NULL, `last_sent_icon` TEXT DEFAULT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, `state_class` TEXT, `entity_category` TEXT, `core_registration` TEXT, `app_registration` TEXT, PRIMARY KEY(`id`, `server_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registered", + "columnName": "registered", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSentState", + "columnName": "last_sent_state", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "lastSentIcon", + "columnName": "last_sent_icon", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "stateType", + "columnName": "state_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceClass", + "columnName": "device_class", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unitOfMeasurement", + "columnName": "unit_of_measurement", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "stateClass", + "columnName": "state_class", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "entityCategory", + "columnName": "entity_category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coreRegistration", + "columnName": "core_registration", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "appRegistration", + "columnName": "app_registration", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "server_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sensor_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `entries` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))", + "fields": [ + { + "fieldPath": "sensorId", + "columnName": "sensor_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueType", + "columnName": "value_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entries", + "columnName": "entries", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sensor_id", + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "button_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT, `icon_name` TEXT NOT NULL, `domain` TEXT NOT NULL, `service` TEXT NOT NULL, `service_data` TEXT NOT NULL, `label` TEXT, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, `require_authentication` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconName", + "columnName": "icon_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "service", + "columnName": "service", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceData", + "columnName": "service_data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "requireAuthentication", + "columnName": "require_authentication", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "camera_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `tap_action` TEXT NOT NULL DEFAULT 'REFRESH', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tapAction", + "columnName": "tap_action", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'REFRESH'" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "media_player_controls_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `label` TEXT, `show_skip` INTEGER NOT NULL, `show_seek` INTEGER NOT NULL, `show_volume` INTEGER NOT NULL, `show_source` INTEGER NOT NULL DEFAULT false, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showSkip", + "columnName": "show_skip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showSeek", + "columnName": "show_seek", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showVolume", + "columnName": "show_volume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showSource", + "columnName": "show_source", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "static_widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `attribute_ids` TEXT, `label` TEXT, `text_size` REAL NOT NULL, `state_separator` TEXT NOT NULL, `attribute_separator` TEXT NOT NULL, `tap_action` TEXT NOT NULL DEFAULT 'REFRESH', `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributeIds", + "columnName": "attribute_ids", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "textSize", + "columnName": "text_size", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "stateSeparator", + "columnName": "state_separator", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributeSeparator", + "columnName": "attribute_separator", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tapAction", + "columnName": "tap_action", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'REFRESH'" + }, + { + "fieldPath": "lastUpdate", + "columnName": "last_update", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "template_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL DEFAULT 'template', `template` TEXT NOT NULL, `text_size` REAL NOT NULL DEFAULT 12.0, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'template'" + }, + { + "fieldPath": "template", + "columnName": "template", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textSize", + "columnName": "text_size", + "affinity": "REAL", + "notNull": true, + "defaultValue": "12.0" + }, + { + "fieldPath": "lastUpdate", + "columnName": "last_update", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `message` TEXT NOT NULL, `data` TEXT NOT NULL, `source` TEXT NOT NULL, `server_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "location_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created` INTEGER NOT NULL, `trigger` TEXT NOT NULL, `result` TEXT NOT NULL, `latitude` REAL, `longitude` REAL, `location_name` TEXT, `accuracy` INTEGER, `data` TEXT, `server_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trigger", + "columnName": "trigger", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "result", + "columnName": "result", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "locationName", + "columnName": "location_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accuracy", + "columnName": "accuracy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "qs_tiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tile_id` TEXT NOT NULL, `added` INTEGER NOT NULL DEFAULT 1, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT, `entity_id` TEXT NOT NULL, `label` TEXT NOT NULL, `subtitle` TEXT, `should_vibrate` INTEGER NOT NULL DEFAULT 0, `auth_required` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tileId", + "columnName": "tile_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "added", + "columnName": "added", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "iconName", + "columnName": "icon_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subtitle", + "columnName": "subtitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shouldVibrate", + "columnName": "should_vibrate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "authRequired", + "columnName": "auth_required", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `friendly_name` TEXT NOT NULL, `icon` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "friendlyName", + "columnName": "friendly_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "camera_tiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT, `refresh_interval` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "refreshInterval", + "columnName": "refresh_interval", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "entity_state_complications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT NOT NULL, `show_title` INTEGER NOT NULL DEFAULT 1, `show_unit` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showTitle", + "columnName": "show_title", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showUnit", + "columnName": "show_unit", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `_name` TEXT NOT NULL, `name_override` TEXT, `_version` TEXT, `device_registry_id` TEXT, `list_order` INTEGER NOT NULL, `device_name` TEXT, `external_url` TEXT NOT NULL, `internal_url` TEXT, `cloud_url` TEXT, `webhook_id` TEXT, `secret` TEXT, `cloudhook_url` TEXT, `use_cloud` INTEGER NOT NULL, `internal_ssids` TEXT NOT NULL, `prioritize_internal` INTEGER NOT NULL, `access_token` TEXT, `refresh_token` TEXT, `token_expiration` INTEGER, `token_type` TEXT, `install_id` TEXT, `user_id` TEXT, `user_name` TEXT, `user_is_owner` INTEGER, `user_is_admin` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_name", + "columnName": "_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nameOverride", + "columnName": "name_override", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "_version", + "columnName": "_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceRegistryId", + "columnName": "device_registry_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "listOrder", + "columnName": "list_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceName", + "columnName": "device_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.externalUrl", + "columnName": "external_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "connection.internalUrl", + "columnName": "internal_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.cloudUrl", + "columnName": "cloud_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.webhookId", + "columnName": "webhook_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.secret", + "columnName": "secret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.cloudhookUrl", + "columnName": "cloudhook_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.useCloud", + "columnName": "use_cloud", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "connection.internalSsids", + "columnName": "internal_ssids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "connection.prioritizeInternal", + "columnName": "prioritize_internal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "session.accessToken", + "columnName": "access_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "session.refreshToken", + "columnName": "refresh_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "session.tokenExpiration", + "columnName": "token_expiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "session.tokenType", + "columnName": "token_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "session.installId", + "columnName": "install_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.name", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.isOwner", + "columnName": "user_is_owner", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "user.isAdmin", + "columnName": "user_is_admin", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websocket_setting` TEXT NOT NULL, `sensor_update_frequency` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "websocketSetting", + "columnName": "websocket_setting", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sensorUpdateFrequency", + "columnName": "sensor_update_frequency", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0c9247ff03d0dd1f6d7ef42c18c369ce')" + ] + } +} \ No newline at end of file diff --git a/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt b/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt index 3f3b64f5b60..df81f5951c6 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt @@ -97,7 +97,7 @@ import kotlinx.coroutines.runBlocking Server::class, Setting::class ], - version = 47, + version = 48, autoMigrations = [ AutoMigration(from = 24, to = 25), AutoMigration(from = 25, to = 26), @@ -120,7 +120,8 @@ import kotlinx.coroutines.runBlocking AutoMigration(from = 43, to = 44), AutoMigration(from = 44, to = 45), AutoMigration(from = 45, to = 46), - AutoMigration(from = 46, to = 47) + AutoMigration(from = 46, to = 47), + AutoMigration(from = 47, to = 48) ] ) @TypeConverters( From 969f070e3704672671e8628fda3f6141913af2b1 Mon Sep 17 00:00:00 2001 From: Ivor Smorenburg Date: Sun, 15 Sep 2024 17:48:20 +0200 Subject: [PATCH 3/9] Revert Accidental edit of schema --- .../47.json | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/common/schemas/io.homeassistant.companion.android.database.AppDatabase/47.json b/common/schemas/io.homeassistant.companion.android.database.AppDatabase/47.json index 1ea33cbdcf7..f9cacda745d 100644 --- a/common/schemas/io.homeassistant.companion.android.database.AppDatabase/47.json +++ b/common/schemas/io.homeassistant.companion.android.database.AppDatabase/47.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 47, - "identityHash": "0c9247ff03d0dd1f6d7ef42c18c369ce", + "identityHash": "f02f3b0062cfc7559bbd23f8148fb3b1", "entities": [ { "tableName": "sensor_attributes", @@ -249,7 +249,7 @@ }, { "tableName": "button_widgets", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT, `icon_name` TEXT NOT NULL, `domain` TEXT NOT NULL, `service` TEXT NOT NULL, `service_data` TEXT NOT NULL, `label` TEXT, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, `require_authentication` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT NOT NULL, `domain` TEXT NOT NULL, `service` TEXT NOT NULL, `service_data` TEXT NOT NULL, `label` TEXT, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, `require_authentication` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -264,12 +264,6 @@ "notNull": true, "defaultValue": "0" }, - { - "fieldPath": "entityId", - "columnName": "entity_id", - "affinity": "TEXT", - "notNull": false - }, { "fieldPath": "iconName", "columnName": "icon_name", @@ -538,7 +532,7 @@ }, { "tableName": "template_widgets", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL DEFAULT 'template', `template` TEXT NOT NULL, `text_size` REAL NOT NULL DEFAULT 12.0, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `template` TEXT NOT NULL, `text_size` REAL NOT NULL DEFAULT 12.0, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -553,13 +547,6 @@ "notNull": true, "defaultValue": "0" }, - { - "fieldPath": "entityId", - "columnName": "entity_id", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'template'" - }, { "fieldPath": "template", "columnName": "template", @@ -1134,7 +1121,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0c9247ff03d0dd1f6d7ef42c18c369ce')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f02f3b0062cfc7559bbd23f8148fb3b1')" ] } } \ No newline at end of file From edfa4f6fee11c9493f41d8defd85c9ff77705d1d Mon Sep 17 00:00:00 2001 From: Ivor Smorenburg Date: Sun, 15 Sep 2024 23:20:30 +0100 Subject: [PATCH 4/9] Fixes https://github.com/home-assistant/android/issues/4641 loading default look and feel Fixes Static Widget Info wrongly using Button Widget Layout --- app/src/main/res/layout/widget_static.xml | 1 + app/src/main/res/xml/entity_widget_info.xml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/widget_static.xml b/app/src/main/res/layout/widget_static.xml index c1fdee9d53c..874e421a0c4 100644 --- a/app/src/main/res/layout/widget_static.xml +++ b/app/src/main/res/layout/widget_static.xml @@ -6,6 +6,7 @@ android:minHeight="40dp" android:minWidth="40dp" android:background="@drawable/widget_button_background" + android:theme="@style/Theme.HomeAssistant.Widget" android:padding="4dp"> Date: Sun, 15 Sep 2024 23:36:15 +0100 Subject: [PATCH 5/9] Fixes https://github.com/home-assistant/android/issues/4641 loading default look and feel for the rest of the widgets Transparent background to camera. --- app/src/main/res/drawable/circle.xml | 14 ++++++++++++++ app/src/main/res/layout/widget_button.xml | 3 ++- app/src/main/res/layout/widget_camera.xml | 19 ++++++++++++------- .../main/res/layout/widget_media_controls.xml | 1 + app/src/main/res/layout/widget_template.xml | 1 + 5 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/drawable/circle.xml diff --git a/app/src/main/res/drawable/circle.xml b/app/src/main/res/drawable/circle.xml new file mode 100644 index 00000000000..25b46b0c152 --- /dev/null +++ b/app/src/main/res/drawable/circle.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/widget_button.xml b/app/src/main/res/layout/widget_button.xml index 9eceab3128b..35168733e3a 100644 --- a/app/src/main/res/layout/widget_button.xml +++ b/app/src/main/res/layout/widget_button.xml @@ -8,7 +8,8 @@ android:minHeight="40dp" android:minWidth="40dp" android:background="@drawable/widget_button_background" - android:padding="4dp"> + android:padding="4dp" + android:theme="@style/Theme.HomeAssistant.Widget"> @@ -21,17 +22,21 @@ - + diff --git a/app/src/main/res/layout/widget_media_controls.xml b/app/src/main/res/layout/widget_media_controls.xml index 52ef5764e76..063d115d700 100644 --- a/app/src/main/res/layout/widget_media_controls.xml +++ b/app/src/main/res/layout/widget_media_controls.xml @@ -7,6 +7,7 @@ android:background="@drawable/widget_button_background" android:clipToOutline="true" android:minWidth="40dp" + android:theme="@style/Theme.HomeAssistant.Widget" android:minHeight="40dp" tools:ignore="UseAppTint"> diff --git a/app/src/main/res/layout/widget_template.xml b/app/src/main/res/layout/widget_template.xml index 39aebfbd7bc..d3d5700502d 100644 --- a/app/src/main/res/layout/widget_template.xml +++ b/app/src/main/res/layout/widget_template.xml @@ -6,6 +6,7 @@ android:background="@drawable/widget_button_background" android:minWidth="40dp" android:minHeight="40dp" + android:theme="@style/Theme.HomeAssistant.Widget" android:padding="4dp"> Date: Mon, 16 Sep 2024 16:28:38 +0100 Subject: [PATCH 6/9] Addressed some comments --- .../repositories/BaseDaoWidgetRepository.kt | 2 +- .../repositories/ButtonWidgetRepository.kt | 2 +- .../ButtonWidgetRepositoryImpl.kt | 2 +- .../repositories/CameraWidgetRepository.kt | 2 +- .../CameraWidgetRepositoryImpl.kt | 2 +- .../MediaPlayerControlsWidgetRepository.kt | 6 ++ ...MediaPlayerControlsWidgetRepositoryImpl.kt | 2 +- .../android}/repositories/RepositoryModule.kt | 2 +- .../repositories/StaticWidgetRepository.kt | 2 +- .../StaticWidgetRepositoryImpl.kt | 2 +- .../repositories/TemplateWidgetRepository.kt | 2 +- .../TemplateWidgetRepositoryImpl.kt | 2 +- .../widgets/BaseWidgetConfigureActivity.kt | 2 +- .../android/widgets/BaseWidgetProvider.kt | 2 +- .../android/widgets/button/ButtonWidget.kt | 2 +- .../button/ButtonWidgetConfigureActivity.kt | 2 +- .../android/widgets/camera/CameraWidget.kt | 100 +++++++----------- .../camera/CameraWidgetConfigureActivity.kt | 2 +- .../android/widgets/entity/EntityWidget.kt | 2 +- .../entity/EntityWidgetConfigureActivity.kt | 2 +- .../mediaplayer/MediaPlayerControlsWidget.kt | 2 +- ...iaPlayerControlsWidgetConfigureActivity.kt | 2 +- .../widgets/template/TemplateWidget.kt | 2 +- .../TemplateWidgetConfigureActivity.kt | 2 +- app/src/main/res/drawable/circle.xml | 14 --- app/src/main/res/layout/widget_button.xml | 4 +- app/src/main/res/layout/widget_camera.xml | 13 +-- .../main/res/layout/widget_media_controls.xml | 52 +++++---- app/src/main/res/layout/widget_static.xml | 1 - app/src/main/res/layout/widget_template.xml | 1 - .../android/common/data/DataModule.kt | 7 +- .../MediaPlayerControlsWidgetRepository.kt | 5 - 32 files changed, 95 insertions(+), 152 deletions(-) rename {common/src/main/java/io/homeassistant/companion/android/common/data => app/src/main/java/io/homeassistant/companion/android}/repositories/BaseDaoWidgetRepository.kt (88%) rename {common/src/main/java/io/homeassistant/companion/android/common/data => app/src/main/java/io/homeassistant/companion/android}/repositories/ButtonWidgetRepository.kt (69%) rename {common/src/main/java/io/homeassistant/companion/android/common/data => app/src/main/java/io/homeassistant/companion/android}/repositories/ButtonWidgetRepositoryImpl.kt (93%) rename {common/src/main/java/io/homeassistant/companion/android/common/data => app/src/main/java/io/homeassistant/companion/android}/repositories/CameraWidgetRepository.kt (69%) rename {common/src/main/java/io/homeassistant/companion/android/common/data => app/src/main/java/io/homeassistant/companion/android}/repositories/CameraWidgetRepositoryImpl.kt (92%) create mode 100644 app/src/main/java/io/homeassistant/companion/android/repositories/MediaPlayerControlsWidgetRepository.kt rename {common/src/main/java/io/homeassistant/companion/android/common/data => app/src/main/java/io/homeassistant/companion/android}/repositories/MediaPlayerControlsWidgetRepositoryImpl.kt (94%) rename {common/src/main/java/io/homeassistant/companion/android/common/data => app/src/main/java/io/homeassistant/companion/android}/repositories/RepositoryModule.kt (92%) rename {common/src/main/java/io/homeassistant/companion/android/common/data => app/src/main/java/io/homeassistant/companion/android}/repositories/StaticWidgetRepository.kt (69%) rename {common/src/main/java/io/homeassistant/companion/android/common/data => app/src/main/java/io/homeassistant/companion/android}/repositories/StaticWidgetRepositoryImpl.kt (93%) rename {common/src/main/java/io/homeassistant/companion/android/common/data => app/src/main/java/io/homeassistant/companion/android}/repositories/TemplateWidgetRepository.kt (70%) rename {common/src/main/java/io/homeassistant/companion/android/common/data => app/src/main/java/io/homeassistant/companion/android}/repositories/TemplateWidgetRepositoryImpl.kt (93%) delete mode 100644 app/src/main/res/drawable/circle.xml delete mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepository.kt diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/BaseDaoWidgetRepository.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/BaseDaoWidgetRepository.kt similarity index 88% rename from common/src/main/java/io/homeassistant/companion/android/common/data/repositories/BaseDaoWidgetRepository.kt rename to app/src/main/java/io/homeassistant/companion/android/repositories/BaseDaoWidgetRepository.kt index a8ffc9e8ee1..2c27fc5e742 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/BaseDaoWidgetRepository.kt +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/BaseDaoWidgetRepository.kt @@ -1,4 +1,4 @@ -package io.homeassistant.companion.android.common.data.repositories +package io.homeassistant.companion.android.repositories import io.homeassistant.companion.android.database.widget.WidgetEntity import kotlinx.coroutines.flow.Flow diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepository.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/ButtonWidgetRepository.kt similarity index 69% rename from common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepository.kt rename to app/src/main/java/io/homeassistant/companion/android/repositories/ButtonWidgetRepository.kt index 481fcb9ed6c..e271eee6148 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepository.kt +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/ButtonWidgetRepository.kt @@ -1,4 +1,4 @@ -package io.homeassistant.companion.android.common.data.repositories +package io.homeassistant.companion.android.repositories import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepositoryImpl.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/ButtonWidgetRepositoryImpl.kt similarity index 93% rename from common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepositoryImpl.kt rename to app/src/main/java/io/homeassistant/companion/android/repositories/ButtonWidgetRepositoryImpl.kt index 3266449da62..5fe937ddfe1 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/ButtonWidgetRepositoryImpl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/ButtonWidgetRepositoryImpl.kt @@ -1,4 +1,4 @@ -package io.homeassistant.companion.android.common.data.repositories +package io.homeassistant.companion.android.repositories import io.homeassistant.companion.android.database.widget.ButtonWidgetDao import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepository.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/CameraWidgetRepository.kt similarity index 69% rename from common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepository.kt rename to app/src/main/java/io/homeassistant/companion/android/repositories/CameraWidgetRepository.kt index f712fc2632f..2636cbe12bd 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepository.kt +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/CameraWidgetRepository.kt @@ -1,4 +1,4 @@ -package io.homeassistant.companion.android.common.data.repositories +package io.homeassistant.companion.android.repositories import io.homeassistant.companion.android.database.widget.CameraWidgetEntity diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepositoryImpl.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/CameraWidgetRepositoryImpl.kt similarity index 92% rename from common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepositoryImpl.kt rename to app/src/main/java/io/homeassistant/companion/android/repositories/CameraWidgetRepositoryImpl.kt index a70f37c1514..6b66772ebde 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/CameraWidgetRepositoryImpl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/CameraWidgetRepositoryImpl.kt @@ -1,4 +1,4 @@ -package io.homeassistant.companion.android.common.data.repositories +package io.homeassistant.companion.android.repositories import io.homeassistant.companion.android.database.widget.CameraWidgetDao import io.homeassistant.companion.android.database.widget.CameraWidgetEntity diff --git a/app/src/main/java/io/homeassistant/companion/android/repositories/MediaPlayerControlsWidgetRepository.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/MediaPlayerControlsWidgetRepository.kt new file mode 100644 index 00000000000..55780c70917 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/MediaPlayerControlsWidgetRepository.kt @@ -0,0 +1,6 @@ +package io.homeassistant.companion.android.repositories + +import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetEntity + +interface MediaPlayerControlsWidgetRepository : + BaseDaoWidgetRepository diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepositoryImpl.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/MediaPlayerControlsWidgetRepositoryImpl.kt similarity index 94% rename from common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepositoryImpl.kt rename to app/src/main/java/io/homeassistant/companion/android/repositories/MediaPlayerControlsWidgetRepositoryImpl.kt index 24058733b97..7c091e2516d 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/MediaPlayerControlsWidgetRepositoryImpl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/MediaPlayerControlsWidgetRepositoryImpl.kt @@ -1,4 +1,4 @@ -package io.homeassistant.companion.android.common.data.repositories +package io.homeassistant.companion.android.repositories import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetDao import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetEntity diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/RepositoryModule.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/RepositoryModule.kt similarity index 92% rename from common/src/main/java/io/homeassistant/companion/android/common/data/repositories/RepositoryModule.kt rename to app/src/main/java/io/homeassistant/companion/android/repositories/RepositoryModule.kt index be6574a07bd..3ccb4af1a90 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/RepositoryModule.kt +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/RepositoryModule.kt @@ -1,4 +1,4 @@ -package io.homeassistant.companion.android.common.data.repositories +package io.homeassistant.companion.android.repositories import dagger.Binds import dagger.Module diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepository.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/StaticWidgetRepository.kt similarity index 69% rename from common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepository.kt rename to app/src/main/java/io/homeassistant/companion/android/repositories/StaticWidgetRepository.kt index eb79b52d779..d5890213fba 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepository.kt +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/StaticWidgetRepository.kt @@ -1,4 +1,4 @@ -package io.homeassistant.companion.android.common.data.repositories +package io.homeassistant.companion.android.repositories import io.homeassistant.companion.android.database.widget.StaticWidgetEntity diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepositoryImpl.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/StaticWidgetRepositoryImpl.kt similarity index 93% rename from common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepositoryImpl.kt rename to app/src/main/java/io/homeassistant/companion/android/repositories/StaticWidgetRepositoryImpl.kt index ddbc5f03cc4..1391255a425 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/StaticWidgetRepositoryImpl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/StaticWidgetRepositoryImpl.kt @@ -1,4 +1,4 @@ -package io.homeassistant.companion.android.common.data.repositories +package io.homeassistant.companion.android.repositories import io.homeassistant.companion.android.database.widget.StaticWidgetDao import io.homeassistant.companion.android.database.widget.StaticWidgetEntity diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepository.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/TemplateWidgetRepository.kt similarity index 70% rename from common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepository.kt rename to app/src/main/java/io/homeassistant/companion/android/repositories/TemplateWidgetRepository.kt index e196c4ceb8c..e5ec45fed80 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepository.kt +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/TemplateWidgetRepository.kt @@ -1,4 +1,4 @@ -package io.homeassistant.companion.android.common.data.repositories +package io.homeassistant.companion.android.repositories import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepositoryImpl.kt b/app/src/main/java/io/homeassistant/companion/android/repositories/TemplateWidgetRepositoryImpl.kt similarity index 93% rename from common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepositoryImpl.kt rename to app/src/main/java/io/homeassistant/companion/android/repositories/TemplateWidgetRepositoryImpl.kt index c04bee81a80..fd59cd5ed68 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/repositories/TemplateWidgetRepositoryImpl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/repositories/TemplateWidgetRepositoryImpl.kt @@ -1,4 +1,4 @@ -package io.homeassistant.companion.android.common.data.repositories +package io.homeassistant.companion.android.repositories import io.homeassistant.companion.android.database.widget.TemplateWidgetDao import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetConfigureActivity.kt index 237294e9f81..8fafa995306 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetConfigureActivity.kt @@ -9,8 +9,8 @@ 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.repositories.BaseDaoWidgetRepository import javax.inject.Inject abstract class BaseWidgetConfigureActivity> : BaseActivity() { diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt index a3d996bb76c..f591fe2d02a 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt @@ -12,10 +12,10 @@ import android.util.Log import android.widget.RemoteViews import androidx.core.content.ContextCompat 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.repositories.BaseDaoWidgetRepository import io.homeassistant.companion.android.util.hasActiveConnection import javax.inject.Inject import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt index 80f924617db..d925ff8f971 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt @@ -30,9 +30,9 @@ import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.data.integration.Entity -import io.homeassistant.companion.android.common.data.repositories.ButtonWidgetRepository import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetBackgroundType +import io.homeassistant.companion.android.repositories.ButtonWidgetRepository import io.homeassistant.companion.android.util.getAttribute import io.homeassistant.companion.android.util.icondialog.getIconByMdiName import io.homeassistant.companion.android.widgets.BaseWidgetProvider diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt index 428a4873b14..9bf4a7be3f1 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt @@ -36,9 +36,9 @@ import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.data.integration.Action import io.homeassistant.companion.android.common.data.integration.Entity -import io.homeassistant.companion.android.common.data.repositories.ButtonWidgetRepository import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.databinding.WidgetButtonConfigureBinding +import io.homeassistant.companion.android.repositories.ButtonWidgetRepository import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel import io.homeassistant.companion.android.util.getHexForColor import io.homeassistant.companion.android.util.icondialog.IconDialogFragment diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt index 6c6a709fcd6..e414339eac7 100755 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt @@ -16,9 +16,9 @@ import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.BuildConfig import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.data.integration.Entity -import io.homeassistant.companion.android.common.data.repositories.CameraWidgetRepository import io.homeassistant.companion.android.database.widget.CameraWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetTapAction +import io.homeassistant.companion.android.repositories.CameraWidgetRepository import io.homeassistant.companion.android.util.DisplayUtils.getScreenWidth import io.homeassistant.companion.android.webview.WebViewActivity import io.homeassistant.companion.android.widgets.BaseWidgetProvider @@ -55,43 +55,18 @@ class CameraWidget : BaseWidgetProvider>() { return RemoteViews(context.packageName, R.layout.widget_camera).apply { val widget = repository.get(appWidgetId) if (widget != null) { - var entityPictureUrl: String? - try { - entityPictureUrl = retrieveCameraImageUrl(widget.serverId, widget.entityId) - setViewVisibility(R.id.widgetCameraError, View.GONE) - } catch (e: Exception) { - Log.e(TAG, "Failed to fetch entity or entity does not exist", e) - setViewVisibility(R.id.widgetCameraError, View.VISIBLE) - entityPictureUrl = null - } - val baseUrl = serverManager.getServer(widget.serverId)?.connection?.getUrl().toString().removeSuffix("/") - val url = "$baseUrl$entityPictureUrl" - if (entityPictureUrl == null) { -// setImageViewResource( -// R.id.widgetCameraImage, -// android.R.drawable.ic_menu_camera -// ) -// setViewVisibility( -// R.id.widgetCameraPlaceholder, -// View.VISIBLE -// ) -// setViewVisibility( -// R.id.widgetCameraImage, -// View.GONE -// ) - } else { - setViewVisibility( - R.id.widgetCameraImage, + val buildImageUrl = buildImageUrl(widget) + + setViewVisibility( + R.id.widgetCameraError, + if (buildImageUrl.isNullOrEmpty()) { View.VISIBLE - ) - setViewVisibility( - R.id.widgetCameraPlaceholder, + } else { View.GONE - ) - Log.d(TAG, "Fetching camera image") - } + } + ) - updateBitmapCameraImage(context, hasActiveConnection, this, appWidgetId, url) + updateBitmapCameraImage(context, hasActiveConnection, this, appWidgetId, buildImageUrl) val tapWidgetPendingIntent = when (widget.tapAction) { WidgetTapAction.OPEN -> PendingIntent.getActivity( @@ -108,23 +83,33 @@ class CameraWidget : BaseWidgetProvider>() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } - setOnClickPendingIntent( - R.id.widgetLayout, - PendingIntent.getBroadcast( - context, - appWidgetId, - updateCameraIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - ) + setOnClickPendingIntent(R.id.widgetLayout, PendingIntent.getBroadcast(context, appWidgetId, updateCameraIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)) setOnClickPendingIntent(R.id.widgetCameraPlaceholder, tapWidgetPendingIntent) } } } + private suspend fun buildImageUrl(cameraWidget: CameraWidgetEntity): String? { + val baseUrl = getServerUrl(cameraWidget.serverId) + val entityPictureUrl = retrieveCameraImageUrl(cameraWidget.serverId, cameraWidget.entityId) + entityPictureUrl?.let { + return "$baseUrl$entityPictureUrl" + } + return null + } + private suspend fun retrieveCameraImageUrl(serverId: Int, entityId: String): String? { - val entity = serverManager.integrationRepository(serverId).getEntity(entityId) - return entity?.attributes?.get(ENTITY_PICTURE_ATTRIBUTE)?.toString() + val entity: Entity>? + try { + entity = serverManager.integrationRepository(serverId).getEntity(entityId) + return entity?.attributes?.get(ENTITY_PICTURE_ATTRIBUTE)?.toString() + } catch (e: Exception) { + return null + } + } + + private fun getServerUrl(serverId: Int): String { + return serverManager.getServer(serverId)?.connection?.getUrl().toString().removeSuffix("/") } override fun onReceive(context: Context, intent: Intent) { @@ -155,18 +140,13 @@ class CameraWidget : BaseWidgetProvider>() { "entity id: " + entitySelection + System.lineSeparator() ) repository.add( - CameraWidgetEntity( - id = appWidgetId, - entityId = entitySelection, - serverId = serverSelection, - tapAction = tapActionSelection - ) + CameraWidgetEntity(id = appWidgetId, entityId = entitySelection, serverId = serverSelection, tapAction = tapActionSelection) ) } } - private fun updateBitmapCameraImage(context: Context, hasActiveConnection: Boolean, views: RemoteViews, appWidgetId: Int, url: String) { - if (hasActiveConnection) { + private fun updateBitmapCameraImage(context: Context, hasActiveConnection: Boolean, views: RemoteViews, appWidgetId: Int, url: String?) { + if (hasActiveConnection && !url.isNullOrEmpty()) { widgetWorkScope?.launch { val picasso = Picasso.get() picasso.isLoggingEnabled = BuildConfig.DEBUG @@ -178,13 +158,16 @@ class CameraWidget : BaseWidgetProvider>() { .onlyScaleDown().get() widgetScope?.launch { + views.setViewVisibility(R.id.widgetCameraImage, View.VISIBLE) + views.setViewVisibility(R.id.widgetCameraError, View.GONE) + views.setViewVisibility(R.id.widgetCameraPlaceholder, View.GONE) views.setImageViewBitmap(R.id.widgetCameraImage, lastCameraBitmap) AppWidgetManager.getInstance(context).partiallyUpdateAppWidget(appWidgetId, views) + Log.d(TAG, "Fetch and load complete") } } catch (e: Exception) { Log.e(TAG, "Unable to fetch image", e) } - Log.d(TAG, "Fetch and load complete") } } else { widgetScope?.launch { @@ -196,12 +179,5 @@ class CameraWidget : BaseWidgetProvider>() { } } -// override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity<*>) { -// super.onEntityStateChanged(context, appWidgetId, entity) -// widgetScope?.launch { -// forceUpdateView(context, appWidgetId) -// } -// } - override suspend fun getUpdates(serverId: Int, entityIds: List): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> } diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt index 4d5e23eaca3..34753b57baf 100755 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt @@ -20,9 +20,9 @@ import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.domain -import io.homeassistant.companion.android.common.data.repositories.CameraWidgetRepository import io.homeassistant.companion.android.database.widget.WidgetTapAction import io.homeassistant.companion.android.databinding.WidgetCameraConfigureBinding +import io.homeassistant.companion.android.repositories.CameraWidgetRepository import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity import io.homeassistant.companion.android.widgets.BaseWidgetProvider.Companion.RECEIVE_DATA diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt index c75dfdfc990..71bcb2105c3 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt @@ -22,10 +22,10 @@ import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.canSupportPrecision import io.homeassistant.companion.android.common.data.integration.friendlyState import io.homeassistant.companion.android.common.data.integration.onEntityPressedWithoutState -import io.homeassistant.companion.android.common.data.repositories.StaticWidgetRepository import io.homeassistant.companion.android.database.widget.StaticWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.database.widget.WidgetTapAction +import io.homeassistant.companion.android.repositories.StaticWidgetRepository import io.homeassistant.companion.android.util.getAttribute import io.homeassistant.companion.android.widgets.BaseWidgetProvider import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidgetConfigureActivity.kt index 6571963f66a..e065352c848 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidgetConfigureActivity.kt @@ -28,10 +28,10 @@ import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.EntityExt import io.homeassistant.companion.android.common.data.integration.domain import io.homeassistant.companion.android.common.data.integration.friendlyName -import io.homeassistant.companion.android.common.data.repositories.StaticWidgetRepository import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.database.widget.WidgetTapAction import io.homeassistant.companion.android.databinding.WidgetStaticConfigureBinding +import io.homeassistant.companion.android.repositories.StaticWidgetRepository import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel import io.homeassistant.companion.android.util.getHexForColor import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt index 945708ca9d6..1538c1f0a4f 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt @@ -22,9 +22,9 @@ import io.homeassistant.companion.android.BuildConfig import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.data.integration.Entity -import io.homeassistant.companion.android.common.data.repositories.MediaPlayerControlsWidgetRepository import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetBackgroundType +import io.homeassistant.companion.android.repositories.MediaPlayerControlsWidgetRepository import io.homeassistant.companion.android.widgets.BaseWidgetProvider import java.util.LinkedList import kotlin.collections.HashMap diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidgetConfigureActivity.kt index 22abb1c5c77..c86d1c24566 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidgetConfigureActivity.kt @@ -19,9 +19,9 @@ import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.domain -import io.homeassistant.companion.android.common.data.repositories.MediaPlayerControlsWidgetRepository import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.databinding.WidgetMediaControlsConfigureBinding +import io.homeassistant.companion.android.repositories.MediaPlayerControlsWidgetRepository import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity import io.homeassistant.companion.android.widgets.BaseWidgetProvider.Companion.RECEIVE_DATA diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidget.kt index b62609a1540..648244a6493 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidget.kt @@ -18,9 +18,9 @@ import com.google.android.material.color.DynamicColors import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.R as commonR -import io.homeassistant.companion.android.common.data.repositories.TemplateWidgetRepository import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetBackgroundType +import io.homeassistant.companion.android.repositories.TemplateWidgetRepository import io.homeassistant.companion.android.util.getAttribute import io.homeassistant.companion.android.widgets.BaseWidgetProvider import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt index 7f6041001ed..41a373d82ee 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt @@ -21,9 +21,9 @@ import androidx.lifecycle.lifecycleScope import com.fasterxml.jackson.databind.JsonMappingException import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.common.R as commonR -import io.homeassistant.companion.android.common.data.repositories.TemplateWidgetRepository import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.databinding.WidgetTemplateConfigureBinding +import io.homeassistant.companion.android.repositories.TemplateWidgetRepository import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel import io.homeassistant.companion.android.util.getHexForColor import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity diff --git a/app/src/main/res/drawable/circle.xml b/app/src/main/res/drawable/circle.xml deleted file mode 100644 index 25b46b0c152..00000000000 --- a/app/src/main/res/drawable/circle.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/widget_button.xml b/app/src/main/res/layout/widget_button.xml index 35168733e3a..2306d9d2e51 100644 --- a/app/src/main/res/layout/widget_button.xml +++ b/app/src/main/res/layout/widget_button.xml @@ -1,6 +1,5 @@ + android:padding="4dp"> + android:minHeight="40dp"> - + diff --git a/app/src/main/res/layout/widget_media_controls.xml b/app/src/main/res/layout/widget_media_controls.xml index 063d115d700..7829af84137 100644 --- a/app/src/main/res/layout/widget_media_controls.xml +++ b/app/src/main/res/layout/widget_media_controls.xml @@ -4,11 +4,10 @@ android:id="@+id/widgetLayout" android:layout_width="match_parent" android:layout_height="match_parent" + android:minHeight="40dp" + android:minWidth="40dp" android:background="@drawable/widget_button_background" android:clipToOutline="true" - android:minWidth="40dp" - android:theme="@style/Theme.HomeAssistant.Widget" - android:minHeight="40dp" tools:ignore="UseAppTint"> + android:layout_width="wrap_content" + android:layout_height="match_parent"> @@ -69,14 +65,14 @@ android:id="@+id/widgetSourceLabel" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_toStartOf="@id/widgetSourceIcon" - android:ellipsize="marquee" android:padding="6dp" android:paddingStart="0dp" android:paddingEnd="0dp" android:singleLine="true" - android:textColor="?colorWidgetOnBackground" + android:ellipsize="marquee" android:visibility="invisible" + android:textColor="?colorWidgetOnBackground" + android:layout_toStartOf="@id/widgetSourceIcon" tools:text="Media player Source" /> + android:visibility="gone" + android:tint="?colorWidgetButton" /> + android:visibility="gone" + android:tint="?colorWidgetButton" /> From 4ecf43dd1ce14b7b6a8417de159997ab93e82de3 Mon Sep 17 00:00:00 2001 From: Ivor Smorenburg Date: Mon, 16 Sep 2024 19:01:41 +0100 Subject: [PATCH 7/9] Addressed some comments Fixed camera duplicated Intent to UPDATE_IMAGE --- .../android/widgets/BaseWidgetProvider.kt | 165 +++++++++--------- .../android/widgets/camera/CameraWidget.kt | 7 +- .../android/widgets/entity/EntityWidget.kt | 1 - app/src/main/res/layout/widget_camera.xml | 14 +- .../main/res/layout/widget_media_controls.xml | 1 - 5 files changed, 91 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt index f591fe2d02a..bc246ed7ad2 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/BaseWidgetProvider.kt @@ -65,51 +65,41 @@ abstract class BaseWidgetProvider, WidgetDataType } } - private suspend fun updateAllWidgets( - context: Context - ) { - 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()) - } - - dbWidgetIds.filter { systemWidgetIds.contains(it) }.forEach { - forceUpdateView(context, it) - } - } - - fun forceUpdateView( + override fun onUpdate( context: Context, - appWidgetId: Int, - appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context) + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray ) { - 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) + // There may be multiple widgets active, so update all of them + for (appWidgetId in appWidgetIds) { + widgetScope?.launch { + forceUpdateView(context, appWidgetId, appWidgetManager) + } } } - private suspend fun getAllWidgetIdsWithEntities(): Map>> = - repository.getAllFlow() - .first() - .associate { - it.id to (it.serverId to listOf(it.entityId.orEmpty())) + 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 onScreenOn(context: Context) { + private fun onScreenOn(context: Context) { setupWidgetScope() if (!serverManager.isRegistered()) return widgetScope!!.launch { @@ -151,8 +141,6 @@ abstract class BaseWidgetProvider, WidgetDataType } } - abstract suspend fun getUpdates(serverId: Int, entityIds: List): Flow? - private fun onScreenOff() { if (thisSetScope) { widgetWorkScope?.cancel() @@ -163,6 +151,56 @@ abstract class BaseWidgetProvider, WidgetDataType } } + private suspend fun updateAllWidgets( + context: Context + ) { + 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( + widgetProvider.shortClassName, + "Found widgets $invalidWidgetIds in database, but not in AppWidgetManager - sending onDeleted" + ) + onDeleted(context, invalidWidgetIds.toIntArray()) + } + + dbWidgetIds.filter { systemWidgetIds.contains(it) }.forEach { + forceUpdateView(context, it) + } + } + + 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) + } + } + + protected fun removeSubscription(appWidgetId: Int) { + widgetEntities.remove(appWidgetId) + widgetJobs[appWidgetId]?.cancel() + widgetJobs.remove(appWidgetId) + } + + private suspend fun getAllWidgetIdsWithEntities(): Map>> = + repository.getAllFlow() + .first() + .associate { + it.id to (it.serverId to listOf(it.entityId.orEmpty())) + } + fun setWidgetBackground(views: RemoteViews, layoutId: Int, widget: ThemeableWidgetEntity?) { when (widget?.backgroundType) { WidgetBackgroundType.TRANSPARENT -> { @@ -175,12 +213,6 @@ abstract class BaseWidgetProvider, WidgetDataType } } - 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) @@ -188,46 +220,6 @@ abstract class BaseWidgetProvider, WidgetDataType } } - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { - // There may be multiple widgets active, so update all of them - for (appWidgetId in appWidgetIds) { - widgetScope?.launch { - forceUpdateView(context, appWidgetId, appWidgetManager) - } - } - } - - 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, @@ -244,6 +236,7 @@ abstract class BaseWidgetProvider, WidgetDataType abstract fun getWidgetProvider(context: Context): ComponentName abstract suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, hasActiveConnection: Boolean, suggestedEntity: WidgetDataType? = null): RemoteViews + abstract suspend fun getUpdates(serverId: Int, entityIds: List): Flow? // A map of widget IDs to [server ID, list of entity IDs] abstract fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt index e414339eac7..8d1d33de9c2 100755 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidget.kt @@ -83,8 +83,7 @@ class CameraWidget : BaseWidgetProvider>() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } - setOnClickPendingIntent(R.id.widgetLayout, PendingIntent.getBroadcast(context, appWidgetId, updateCameraIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)) - setOnClickPendingIntent(R.id.widgetCameraPlaceholder, tapWidgetPendingIntent) + setOnClickPendingIntent(R.id.widgetLayout, tapWidgetPendingIntent) } } } @@ -179,5 +178,9 @@ class CameraWidget : BaseWidgetProvider>() { } } + override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity<*>) { + super.onEntityStateChanged(context, appWidgetId, entity) + } + override suspend fun getUpdates(serverId: Int, entityIds: List): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> } diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt index 71bcb2105c3..fcd653e9cae 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/entity/EntityWidget.kt @@ -239,7 +239,6 @@ class EntityWidget : BaseWidgetProvider + android:background="@drawable/widget_transparent_background" + android:theme="@style/Theme.HomeAssistant.Widget"> + android:scaleType="fitCenter" /> diff --git a/app/src/main/res/layout/widget_media_controls.xml b/app/src/main/res/layout/widget_media_controls.xml index 7829af84137..28ef61d4fcf 100644 --- a/app/src/main/res/layout/widget_media_controls.xml +++ b/app/src/main/res/layout/widget_media_controls.xml @@ -27,7 +27,6 @@ android:contentDescription="@string/widget_media_image_description" android:adjustViewBounds="true" android:scaleType="centerCrop" - android:src="@drawable/app_icon_round" android:visibility="invisible" /> Date: Mon, 16 Sep 2024 19:19:54 +0100 Subject: [PATCH 8/9] Addressed some comments --- .../android/widgets/button/ButtonWidget.kt | 118 ++++++++---------- .../android/widgets/camera/CameraWidget.kt | 12 +- .../mediaplayer/MediaPlayerControlsWidget.kt | 84 ++++++------- 3 files changed, 102 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt index d925ff8f971..4d0cc8700e9 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt @@ -64,8 +64,19 @@ class ButtonWidget : BaseWidgetProvider>?): RemoteViews { // Every time AppWidgetManager.updateAppWidget(...) is called, the button listener // and label need to be re-assigned, or the next time the layout updates @@ -88,7 +99,6 @@ class ButtonWidget : BaseWidgetProvider): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> - private fun authThenCallConfiguredAction(context: Context, appWidgetId: Int) { - Log.d(TAG, "Calling authentication, then configured action") - - val intent = Intent(context, WidgetAuthenticationActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) - context.startActivity(intent) - } - private fun setLabelVisibility(views: RemoteViews, widget: ButtonWidgetEntity?) { val labelVisibility = if (widget?.label.isNullOrBlank()) View.GONE else View.VISIBLE views.setViewVisibility(R.id.widgetLabelLayout, labelVisibility) @@ -306,7 +253,7 @@ class ButtonWidget : BaseWidgetProvider>() { "entity id: " + entitySelection + System.lineSeparator() ) repository.add( - CameraWidgetEntity(id = appWidgetId, entityId = entitySelection, serverId = serverSelection, tapAction = tapActionSelection) + CameraWidgetEntity( + appWidgetId, + serverSelection, + entitySelection, + tapActionSelection + ) ) + forceUpdateView(context, appWidgetId) } } @@ -178,9 +184,5 @@ class CameraWidget : BaseWidgetProvider>() { } } - override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity<*>) { - super.onEntityStateChanged(context, appWidgetId, entity) - } - override suspend fun getUpdates(serverId: Int, entityIds: List): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> } diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt index 1538c1f0a4f..7b17cff3b18 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/mediaplayer/MediaPlayerControlsWidget.kt @@ -368,6 +368,46 @@ class MediaPlayerControlsWidget : BaseWidgetProvider, suggestedEntity: Entity>?): Entity>? { + val entity: Entity>? + try { + entity = if (suggestedEntity != null && entityIds.contains(suggestedEntity.entityId)) { + suggestedEntity + } else { + val entities: LinkedList>?> = LinkedList() + entityIds.forEach { + val e = serverManager.integrationRepository(serverId).getEntity(it) + if (e?.state == "playing") return e + entities.add(e) + } + return entities[0] + } + } catch (e: Exception) { + Log.d(TAG, "Failed to fetch entity or entity does not exist") + if (lastIntent == UPDATE_MEDIA_IMAGE) { + Toast.makeText(context, commonR.string.widget_entity_fetch_error, Toast.LENGTH_LONG).show() + } + return null + } + + return entity + } + + override fun onReceive(context: Context, intent: Intent) { + val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + super.onReceive(context, intent) + when (lastIntent) { + UPDATE_MEDIA_IMAGE -> forceUpdateView(context, appWidgetId) + CALL_PREV_TRACK -> callPreviousTrackAction(context, appWidgetId) + CALL_REWIND -> callRewindAction(context, appWidgetId) + CALL_PLAYPAUSE -> callPlayPauseAction(context, appWidgetId) + CALL_FASTFORWARD -> callFastForwardAction(context, appWidgetId) + CALL_NEXT_TRACK -> callNextTrackAction(context, appWidgetId) + CALL_VOLUME_DOWN -> callVolumeDownAction(context, appWidgetId) + CALL_VOLUME_UP -> callVolumeUpAction(context, appWidgetId) + } + } + override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { if (extras == null) return @@ -410,21 +450,6 @@ class MediaPlayerControlsWidget : BaseWidgetProvider forceUpdateView(context, appWidgetId) - CALL_PREV_TRACK -> callPreviousTrackAction(context, appWidgetId) - CALL_REWIND -> callRewindAction(context, appWidgetId) - CALL_PLAYPAUSE -> callPlayPauseAction(context, appWidgetId) - CALL_FASTFORWARD -> callFastForwardAction(context, appWidgetId) - CALL_NEXT_TRACK -> callNextTrackAction(context, appWidgetId) - CALL_VOLUME_DOWN -> callVolumeDownAction(context, appWidgetId) - CALL_VOLUME_UP -> callVolumeUpAction(context, appWidgetId) - } - } - override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity>) { super.onEntityStateChanged(context, appWidgetId, entity) repository.get(appWidgetId)?.let { @@ -434,33 +459,6 @@ class MediaPlayerControlsWidget : BaseWidgetProvider): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> - - private suspend fun getEntity(context: Context, serverId: Int, entityIds: List, suggestedEntity: Entity>?): Entity>? { - val entity: Entity>? - try { - entity = if (suggestedEntity != null && entityIds.contains(suggestedEntity.entityId)) { - suggestedEntity - } else { - val entities: LinkedList>?> = LinkedList() - entityIds.forEach { - val e = serverManager.integrationRepository(serverId).getEntity(it) - if (e?.state == "playing") return e - entities.add(e) - } - return entities[0] - } - } catch (e: Exception) { - Log.d(TAG, "Failed to fetch entity or entity does not exist") - if (lastIntent == UPDATE_MEDIA_IMAGE) { - Toast.makeText(context, commonR.string.widget_entity_fetch_error, Toast.LENGTH_LONG).show() - } - return null - } - - return entity - } - private fun callPreviousTrackAction(context: Context, appWidgetId: Int) { widgetScope?.launch { Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId") @@ -714,4 +712,6 @@ class MediaPlayerControlsWidget : BaseWidgetProvider): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> } From 37055382df5f3738c0cce37a7677a4887de51638 Mon Sep 17 00:00:00 2001 From: Ivor Smorenburg Date: Tue, 17 Sep 2024 16:52:13 +0100 Subject: [PATCH 9/9] Addressed some comments --- app/src/main/AndroidManifest.xml | 6 +- .../android/HomeAssistantApplication.kt | 3 - .../android/widgets/button/ButtonWidget.kt | 147 +++++++++-- .../button/ButtonWidgetConfigureActivity.kt | 2 +- .../android/widgets/camera/CameraWidget.kt | 246 ++++++++++++------ .../camera/CameraWidgetConfigureActivity.kt | 2 +- .../widgets/template/TemplateWidget.kt | 214 ++++++++++++--- .../TemplateWidgetConfigureActivity.kt | 2 +- app/src/main/res/layout/widget_button.xml | 1 + app/src/main/res/layout/widget_camera.xml | 33 +-- .../main/res/layout/widget_media_controls.xml | 3 +- app/src/main/res/xml/entity_widget_info.xml | 4 +- automotive/src/main/AndroidManifest.xml | 6 +- .../companion/android/util/DisplayUtils.kt | 10 - 14 files changed, 492 insertions(+), 187 deletions(-) delete mode 100644 common/src/main/java/io/homeassistant/companion/android/util/DisplayUtils.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3e55c88586a..79842dffd10 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -112,7 +112,7 @@ - + - + @@ -174,7 +174,7 @@ - + >>() { +class ButtonWidget : AppWidgetProvider() { companion object { private const val TAG = "ButtonWidget" const val CALL_SERVICE = "io.homeassistant.companion.android.widgets.button.ButtonWidget.CALL_SERVICE" private const val CALL_SERVICE_AUTH = "io.homeassistant.companion.android.widgets.button.ButtonWidget.CALL_SERVICE_AUTH" + internal const val RECEIVE_DATA = + "io.homeassistant.companion.android.widgets.button.ButtonWidget.RECEIVE_DATA" internal const val EXTRA_SERVER_ID = "EXTRA_SERVER_ID" internal const val EXTRA_DOMAIN = "EXTRA_DOMAIN" @@ -64,9 +69,91 @@ class ButtonWidget : BaseWidgetProvider authThenCallConfiguredAction(context, appWidgetId) + CALL_SERVICE -> callConfiguredAction(context, appWidgetId) + RECEIVE_DATA -> saveActionCallConfiguration(context, intent.extras, appWidgetId) + Intent.ACTION_SCREEN_ON -> updateAllWidgets(context) + } + } private fun authThenCallConfiguredAction(context: Context, appWidgetId: Int) { Log.d(TAG, "Calling authentication, then configured action") @@ -77,13 +164,13 @@ class ButtonWidget : BaseWidgetProvider>?): RemoteViews { + private fun getWidgetRemoteViews(context: Context, appWidgetId: Int): RemoteViews { // Every time AppWidgetManager.updateAppWidget(...) is called, the button listener // and label need to be re-assigned, or the next time the layout updates // (e.g home screen rotation) the widget will fall back on its default layout // without any click listener being applied - val widget = repository.get(appWidgetId) + val widget = buttonWidgetDao.get(appWidgetId) val auth = widget?.requireAuthentication == true val intent = Intent(context, ButtonWidget::class.java).apply { @@ -99,7 +186,7 @@ class ButtonWidget : BaseWidgetProvider authThenCallConfiguredAction(context, appWidgetId) - CALL_SERVICE -> callConfiguredAction(context, appWidgetId) + private fun setWidgetBackground(views: RemoteViews, widget: ButtonWidgetEntity?) { + when (widget?.backgroundType) { + WidgetBackgroundType.TRANSPARENT -> { + views.setInt(R.id.widgetLayout, "setBackgroundColor", Color.TRANSPARENT) + } + else -> { + views.setInt(R.id.widgetLayout, "setBackgroundResource", R.drawable.widget_button_background) + } } } - override suspend fun getUpdates(serverId: Int, entityIds: List): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> - private fun setLabelVisibility(views: RemoteViews, widget: ButtonWidgetEntity?) { val labelVisibility = if (widget?.label.isNullOrBlank()) View.GONE else View.VISIBLE views.setViewVisibility(R.id.widgetLabelLayout, labelVisibility) @@ -186,9 +272,9 @@ class ButtonWidget : BaseWidgetProvider>() { +class CameraWidget : AppWidgetProvider() { companion object { private const val TAG = "CameraWidget" - + internal const val RECEIVE_DATA = + "io.homeassistant.companion.android.widgets.camera.CameraWidget.RECEIVE_DATA" internal const val UPDATE_IMAGE = "io.homeassistant.companion.android.widgets.camera.CameraWidget.UPDATE_IMAGE" - internal const val ENTITY_PICTURE_ATTRIBUTE = "entity_picture" - internal const val EXTRA_SERVER_ID = "EXTRA_SERVER_ID" internal const val EXTRA_ENTITY_ID = "EXTRA_ENTITY_ID" internal const val EXTRA_TAP_ACTION = "EXTRA_TAP_ACTION" + private var lastIntent = "" } - private var lastCameraBitmap: Bitmap? = null + @Inject + lateinit var serverManager: ServerManager + + @Inject + lateinit var cameraWidgetDao: CameraWidgetDao + + private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + // There may be multiple widgets active, so update all of them + appWidgetIds.forEach { appWidgetId -> + updateAppWidget( + context, + appWidgetId, + appWidgetManager + ) + } + } + + private fun updateAppWidget( + context: Context, + appWidgetId: Int, + appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context) + ) { + if (!context.hasActiveConnection()) { + Log.d(TAG, "Skipping widget update since network connection is not active") + return + } + mainScope.launch { + val views = getWidgetRemoteViews(context, appWidgetId) + appWidgetManager.updateAppWidget(appWidgetId, views) + } + } + + private fun updateAllWidgets(context: Context) { + mainScope.launch { + val appWidgetManager = AppWidgetManager.getInstance(context) + val systemWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, CameraWidget::class.java)) + val dbWidgetList = cameraWidgetDao.getAll() + + val invalidWidgetIds = dbWidgetList + .filter { !systemWidgetIds.contains(it.id) } + .map { it.id } + if (invalidWidgetIds.isNotEmpty()) { + Log.i(TAG, "Found widgets $invalidWidgetIds in database, but not in AppWidgetManager - sending onDeleted") + onDeleted(context, invalidWidgetIds.toIntArray()) + } - override fun getWidgetProvider(context: Context): ComponentName = - ComponentName(context, CameraWidget::class.java) + val cameraWidgetList = dbWidgetList.filter { systemWidgetIds.contains(it.id) } + if (cameraWidgetList.isNotEmpty()) { + Log.d(TAG, "Updating all widgets") + for (item in cameraWidgetList) { + updateAppWidget(context, item.id, appWidgetManager) + } + } + } + } - override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, hasActiveConnection: Boolean, suggestedEntity: Entity<*>?): RemoteViews { + private suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int): RemoteViews { val updateCameraIntent = Intent(context, CameraWidget::class.java).apply { action = UPDATE_IMAGE putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) } return RemoteViews(context.packageName, R.layout.widget_camera).apply { - val widget = repository.get(appWidgetId) + val widget = cameraWidgetDao.get(appWidgetId) if (widget != null) { - val buildImageUrl = buildImageUrl(widget) - - setViewVisibility( - R.id.widgetCameraError, - if (buildImageUrl.isNullOrEmpty()) { + var entityPictureUrl: String? + try { + entityPictureUrl = retrieveCameraImageUrl(widget.serverId, widget.entityId) + setViewVisibility(R.id.widgetCameraError, View.GONE) + } catch (e: Exception) { + Log.e(TAG, "Failed to fetch entity or entity does not exist", e) + setViewVisibility(R.id.widgetCameraError, View.VISIBLE) + entityPictureUrl = null + } + val baseUrl = serverManager.getServer(widget.serverId)?.connection?.getUrl().toString().removeSuffix("/") + val url = "$baseUrl$entityPictureUrl" + if (entityPictureUrl == null) { + setImageViewResource( + R.id.widgetCameraImage, + R.drawable.app_icon_round + ) + setViewVisibility( + R.id.widgetCameraPlaceholder, View.VISIBLE - } else { + ) + setViewVisibility( + R.id.widgetCameraImage, + View.GONE + ) + } else { + setViewVisibility( + R.id.widgetCameraImage, + View.VISIBLE + ) + setViewVisibility( + R.id.widgetCameraPlaceholder, View.GONE + ) + Log.d(TAG, "Fetching camera image") + Handler(Looper.getMainLooper()).post { + val picasso = Picasso.get() + if (BuildConfig.DEBUG) { + picasso.isLoggingEnabled = true + } + try { + picasso.invalidate(url) + picasso.load(url).resize(getScreenWidth(), 0).onlyScaleDown().into( + this, + R.id.widgetCameraImage, + intArrayOf(appWidgetId) + ) + } catch (e: Exception) { + Log.e(TAG, "Unable to fetch image", e) + } + Log.d(TAG, "Fetch and load complete") } - ) - - updateBitmapCameraImage(context, hasActiveConnection, this, appWidgetId, buildImageUrl) + } val tapWidgetPendingIntent = when (widget.tapAction) { WidgetTapAction.OPEN -> PendingIntent.getActivity( @@ -83,43 +185,37 @@ class CameraWidget : BaseWidgetProvider>() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } - setOnClickPendingIntent(R.id.widgetLayout, tapWidgetPendingIntent) + setOnClickPendingIntent(R.id.widgetCameraImage, tapWidgetPendingIntent) + setOnClickPendingIntent(R.id.widgetCameraPlaceholder, tapWidgetPendingIntent) } } } - private suspend fun buildImageUrl(cameraWidget: CameraWidgetEntity): String? { - val baseUrl = getServerUrl(cameraWidget.serverId) - val entityPictureUrl = retrieveCameraImageUrl(cameraWidget.serverId, cameraWidget.entityId) - entityPictureUrl?.let { - return "$baseUrl$entityPictureUrl" - } - return null - } - private suspend fun retrieveCameraImageUrl(serverId: Int, entityId: String): String? { - val entity: Entity>? - try { - entity = serverManager.integrationRepository(serverId).getEntity(entityId) - return entity?.attributes?.get(ENTITY_PICTURE_ATTRIBUTE)?.toString() - } catch (e: Exception) { - return null - } - } - - private fun getServerUrl(serverId: Int): String { - return serverManager.getServer(serverId)?.connection?.getUrl().toString().removeSuffix("/") + val entity = serverManager.integrationRepository(serverId).getEntity(entityId) + return entity?.attributes?.get("entity_picture")?.toString() } override fun onReceive(context: Context, intent: Intent) { - super.onReceive(context, intent) + lastIntent = intent.action.toString() val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + + Log.d( + TAG, + "Broadcast received: " + System.lineSeparator() + + "Broadcast action: " + lastIntent + System.lineSeparator() + + "AppWidgetId: " + appWidgetId + ) + + super.onReceive(context, intent) when (lastIntent) { - UPDATE_IMAGE -> forceUpdateView(context, appWidgetId) + RECEIVE_DATA -> saveEntityConfiguration(context, intent.extras, appWidgetId) + UPDATE_IMAGE -> updateAppWidget(context, appWidgetId) + Intent.ACTION_SCREEN_ON -> updateAllWidgets(context) } } - override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { + private fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { if (extras == null) return val serverSelection = if (extras.containsKey(EXTRA_SERVER_ID)) extras.getInt(EXTRA_SERVER_ID) else null @@ -132,13 +228,13 @@ class CameraWidget : BaseWidgetProvider>() { return } - widgetScope?.launch { + mainScope.launch { Log.d( TAG, "Saving camera config data:" + System.lineSeparator() + "entity id: " + entitySelection + System.lineSeparator() ) - repository.add( + cameraWidgetDao.add( CameraWidgetEntity( appWidgetId, serverSelection, @@ -146,43 +242,27 @@ class CameraWidget : BaseWidgetProvider>() { tapActionSelection ) ) - forceUpdateView(context, appWidgetId) + + onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId)) } } - private fun updateBitmapCameraImage(context: Context, hasActiveConnection: Boolean, views: RemoteViews, appWidgetId: Int, url: String?) { - if (hasActiveConnection && !url.isNullOrEmpty()) { - widgetWorkScope?.launch { - val picasso = Picasso.get() - picasso.isLoggingEnabled = BuildConfig.DEBUG - try { - picasso.invalidate(url) - lastCameraBitmap = picasso.load(url) - .stableKey(url) - .resize(getScreenWidth(), 0) - .onlyScaleDown().get() - - widgetScope?.launch { - views.setViewVisibility(R.id.widgetCameraImage, View.VISIBLE) - views.setViewVisibility(R.id.widgetCameraError, View.GONE) - views.setViewVisibility(R.id.widgetCameraPlaceholder, View.GONE) - views.setImageViewBitmap(R.id.widgetCameraImage, lastCameraBitmap) - AppWidgetManager.getInstance(context).partiallyUpdateAppWidget(appWidgetId, views) - Log.d(TAG, "Fetch and load complete") - } - } catch (e: Exception) { - Log.e(TAG, "Unable to fetch image", e) - } - } - } else { - widgetScope?.launch { - lastCameraBitmap?.let { - views.setImageViewBitmap(R.id.widgetCameraImage, it) - AppWidgetManager.getInstance(context).partiallyUpdateAppWidget(appWidgetId, views) - } - } + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + // When the user deletes the widget, delete the preference associated with it. + mainScope.launch { + cameraWidgetDao.deleteAll(appWidgetIds) } } - override suspend fun getUpdates(serverId: Int, entityIds: List): Flow>> = serverManager.integrationRepository(serverId).getEntityUpdates(entityIds) as Flow>> + override fun onEnabled(context: Context) { + // Enter relevant functionality for when the first widget is created + } + + override fun onDisabled(context: Context) { + // Enter relevant functionality for when the last widget is disabled + } + + private fun getScreenWidth(): Int { + return Resources.getSystem().displayMetrics.widthPixels + } } diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt index 34753b57baf..2b4f42bf9a1 100755 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/camera/CameraWidgetConfigureActivity.kt @@ -187,7 +187,7 @@ class CameraWidgetConfigureActivity : BaseWidgetConfigureActivity() { - +class TemplateWidget : AppWidgetProvider() { companion object { private const val TAG = "TemplateWidget" + const val UPDATE_VIEW = + "io.homeassistant.companion.android.widgets.template.TemplateWidget.UPDATE_VIEW" + const val RECEIVE_DATA = + "io.homeassistant.companion.android.widgets.template.TemplateWidget.RECEIVE_DATA" + internal const val EXTRA_SERVER_ID = "EXTRA_SERVER_ID" internal const val EXTRA_TEMPLATE = "extra_template" internal const val EXTRA_TEXT_SIZE = "EXTRA_TEXT_SIZE" internal const val EXTRA_BACKGROUND_TYPE = "EXTRA_BACKGROUND_TYPE" internal const val EXTRA_TEXT_COLOR = "EXTRA_TEXT_COLOR" + + private var widgetScope: CoroutineScope? = null + private val widgetTemplates = mutableMapOf() + private val widgetJobs = mutableMapOf() + } + + @Inject + lateinit var serverManager: ServerManager + + @Inject + lateinit var templateWidgetDao: TemplateWidgetDao + + private var thisSetScope = false + private var lastIntent = "" + + init { + setupWidgetScope() + } + + private fun setupWidgetScope() { + if (widgetScope == null || !widgetScope!!.isActive) { + widgetScope = CoroutineScope(Dispatchers.Main + Job()) + thisSetScope = true + } + } + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + // 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) + } + } + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + // When the user deletes the widget, delete the preference associated with it. + widgetScope?.launch { + templateWidgetDao.deleteAll(appWidgetIds) + appWidgetIds.forEach { + widgetTemplates.remove(it) + widgetJobs[it]?.cancel() + widgetJobs.remove(it) + } + } + } + + override fun onReceive(context: Context, intent: Intent) { + lastIntent = intent.action.toString() + val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + + 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() + } + } + + private fun onScreenOn(context: Context) { + setupWidgetScope() + if (!serverManager.isRegistered()) return + widgetScope!!.launch { + updateAllWidgets(context) + + val allWidgets = templateWidgetDao.getAll() + val widgetsWithDifferentTemplate = allWidgets.filter { it.template != widgetTemplates[it.id] } + if (widgetsWithDifferentTemplate.isNotEmpty()) { + if (thisSetScope) { + ContextCompat.registerReceiver( + context.applicationContext, + this@TemplateWidget, + IntentFilter(Intent.ACTION_SCREEN_OFF), + ContextCompat.RECEIVER_NOT_EXPORTED + ) + } + + widgetsWithDifferentTemplate.forEach { widget -> + widgetJobs[widget.id]?.cancel() + + val templateUpdates = + if (serverManager.getServer(widget.serverId) != null) { + serverManager.integrationRepository(widget.serverId).getTemplateUpdates(widget.template) + } else { + null + } + if (templateUpdates != null) { + widgetTemplates[widget.id] = widget.template + widgetJobs[widget.id] = widgetScope!!.launch { + templateUpdates.collect { + onTemplateChanged(context, widget.id, it) + } + } + } else { // Remove data to make it retry on the next update + widgetTemplates.remove(widget.id) + widgetJobs.remove(widget.id) + } + } + } + } } - override fun getWidgetProvider(context: Context): ComponentName = - ComponentName(context, TemplateWidget::class.java) + private fun onScreenOff() { + if (thisSetScope) { + widgetScope?.cancel() + thisSetScope = false + widgetTemplates.clear() + widgetJobs.clear() + } + } - override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, hasActiveConnection: Boolean, suggestedEntity: String?): RemoteViews { + private suspend fun updateAllWidgets( + context: Context + ) { + val systemWidgetIds = AppWidgetManager.getInstance(context) + .getAppWidgetIds(ComponentName(context, TemplateWidget::class.java)) + .toSet() + val dbWidgetIds = templateWidgetDao.getAll().map { it.id } + + val invalidWidgetIds = dbWidgetIds.minus(systemWidgetIds) + if (invalidWidgetIds.isNotEmpty()) { + Log.i(TAG, "Found widgets $invalidWidgetIds in database, but not in AppWidgetManager - sending onDeleted") + onDeleted(context, invalidWidgetIds.toIntArray()) + } + + dbWidgetIds.filter { systemWidgetIds.contains(it) }.forEach { + updateView(context, it) + } + } + + private fun updateView( + context: Context, + appWidgetId: Int, + appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context) + ) { + widgetScope?.launch { + val views = getWidgetRemoteViews(context, appWidgetId) + appWidgetManager.updateAppWidget(appWidgetId, views) + } + } + + private suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedTemplate: String? = null): RemoteViews { // Every time AppWidgetManager.updateAppWidget(...) is called, the button listener // and label need to be re-assigned, or the next time the layout updates // (e.g home screen rotation) the widget will fall back on its default layout @@ -53,7 +214,7 @@ class TemplateWidget : BaseWidgetProvider() { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) } - val widget = repository.get(appWidgetId) + val widget = templateWidgetDao.get(appWidgetId) val useDynamicColors = widget?.backgroundType == WidgetBackgroundType.DYNAMICCOLOR && DynamicColors.isDynamicColorAvailable() return RemoteViews(context.packageName, if (useDynamicColors) R.layout.widget_template_wrapper_dynamiccolor else R.layout.widget_template_wrapper_default).apply { @@ -72,16 +233,15 @@ class TemplateWidget : BaseWidgetProvider() { var textColor = context.getAttribute(R.attr.colorWidgetOnBackground, ContextCompat.getColor(context, commonR.color.colorWidgetButtonLabel)) widget.textColor?.let { textColor = it.toColorInt() } + setInt(R.id.widgetLayout, "setBackgroundColor", Color.TRANSPARENT) setTextColor(R.id.widgetTemplateText, textColor) } - setWidgetBackground(this, R.id.widgetLayout, widget) - // Content - var renderedTemplate: String? = repository.get(appWidgetId)?.lastUpdate ?: context.getString(commonR.string.loading) + var renderedTemplate: String? = templateWidgetDao.get(appWidgetId)?.lastUpdate ?: context.getString(commonR.string.loading) try { - renderedTemplate = suggestedEntity ?: serverManager.integrationRepository(widget.serverId).renderTemplate(widget.template, mapOf()).toString() - repository.updateWidgetLastUpdate( + renderedTemplate = suggestedTemplate ?: serverManager.integrationRepository(widget.serverId).renderTemplate(widget.template, mapOf()).toString() + templateWidgetDao.updateTemplateWidgetLastUpdate( appWidgetId, renderedTemplate ) @@ -105,19 +265,7 @@ class TemplateWidget : BaseWidgetProvider() { } } - override fun onWidgetsViewUpdated(context: Context, appWidgetId: Int, appWidgetManager: AppWidgetManager, remoteViews: RemoteViews, hasActiveConnection: Boolean) { - super.onWidgetsViewUpdated(context, appWidgetId, appWidgetManager, remoteViews, hasActiveConnection) - remoteViews.setViewVisibility( - R.id.widgetTemplateError, - if (!hasActiveConnection) View.VISIBLE else View.GONE - ) - } - - override suspend fun getUpdates(serverId: Int, entityIds: List): Flow? { - return serverManager.integrationRepository(serverId).getTemplateUpdates(entityIds.first()) - } - - override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { + private fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { if (extras == null) return val serverId = if (extras.containsKey(EXTRA_SERVER_ID)) extras.getInt(EXTRA_SERVER_ID) else null @@ -133,18 +281,26 @@ class TemplateWidget : BaseWidgetProvider() { } widgetScope?.launch { - repository.add( + templateWidgetDao.add( TemplateWidgetEntity( appWidgetId, serverId, "template_$appWidgetId", template, textSize, - repository.get(appWidgetId)?.lastUpdate ?: ContextCompat.getString(context, commonR.string.loading), + templateWidgetDao.get(appWidgetId)?.lastUpdate ?: "Loading", backgroundTypeSelection, textColorSelection ) ) + onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId)) + } + } + + private fun onTemplateChanged(context: Context, appWidgetId: Int, template: String?) { + widgetScope?.launch { + val views = getWidgetRemoteViews(context, appWidgetId, template) + AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views) } } } diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt index 41a373d82ee..78af90b234a 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt @@ -167,7 +167,7 @@ class TemplateWidgetConfigureActivity : BaseWidgetConfigureActivity - - - - - - + android:layout_height="match_parent" + android:layout_marginStart="4dp" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:src="@drawable/app_icon_round" + android:contentDescription="@string/widget_camera_contentdescription" + android:scaleType="fitCenter" /> + android:layout_gravity="center_horizontal|bottom" + android:visibility="gone"/> \ No newline at end of file diff --git a/app/src/main/res/layout/widget_media_controls.xml b/app/src/main/res/layout/widget_media_controls.xml index 28ef61d4fcf..5e90b7d0122 100644 --- a/app/src/main/res/layout/widget_media_controls.xml +++ b/app/src/main/res/layout/widget_media_controls.xml @@ -33,14 +33,13 @@ android:id="@+id/widgetMediaPlaceholder" android:layout_width="wrap_content" android:layout_height="match_parent" - android:padding="30dp" android:layout_marginStart="4dp" android:layout_marginTop="4dp" android:layout_marginBottom="4dp" android:contentDescription="@string/widget_media_image_description" android:adjustViewBounds="true" android:scaleType="fitCenter" - android:src="@android:drawable/ic_media_play" /> + android:src="@drawable/app_icon_round" /> - + - + @@ -182,7 +182,7 @@ - +