From ec8d0f54eed3c6ed7c17e77a6a0ef1f37f8aeb5d Mon Sep 17 00:00:00 2001 From: Christophe Gagnier Date: Thu, 15 Jan 2026 00:57:26 -0500 Subject: [PATCH] feat(widget): Add widget preview for Android 15 This commit adds support for widget previews, a feature introduced in Android 15 (Vanilla Ice Cream). - A `WidgetPreviewPublisher` is added to manage the publishing of widget previews to the launcher, handling versioning to avoid unnecessary updates. - The `Glance` library is updated to `1.2.0-rc01` to support the new preview APIs. - The `WledWidget` now implements `providePreview` to render a static preview in the widget picker. - The widget's description and name strings have been updated to be more descriptive. - The widget's metadata (`wled_widget_info.xml`) has been adjusted for better sizing in the launcher. --- app/src/main/AndroidManifest.xml | 3 +- .../wlednativeandroid/DevicesApplication.kt | 9 ++- .../wlednativeandroid/widget/WidgetContent.kt | 34 +++++---- .../widget/WidgetPreviewPublisher.kt | 72 +++++++++++++++++++ .../wlednativeandroid/widget/WledWidget.kt | 8 +++ app/src/main/res/values-de/strings.xml | 3 +- app/src/main/res/values-fr/strings.xml | 3 +- app/src/main/res/values-zh/strings.xml | 3 +- app/src/main/res/values/strings.xml | 3 +- app/src/main/res/xml/wled_widget_info.xml | 4 +- gradle/libs.versions.toml | 2 +- 11 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WidgetPreviewPublisher.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9acb906..8f257f5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,7 +49,8 @@ + android:exported="true" + android:label="@string/widget_single_device_name"> diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/DevicesApplication.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/DevicesApplication.kt index eb014cb..4391d11 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/DevicesApplication.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/DevicesApplication.kt @@ -1,7 +1,14 @@ package ca.cgagnier.wlednativeandroid import android.app.Application +import ca.cgagnier.wlednativeandroid.widget.WidgetPreviewPublisher import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class DevicesApplication : Application() +class DevicesApplication : Application() { + + override fun onCreate() { + super.onCreate() + WidgetPreviewPublisher.publishIfNeeded(this) + } +} diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WidgetContent.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WidgetContent.kt index 070ff87..b5a718a 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WidgetContent.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WidgetContent.kt @@ -44,7 +44,7 @@ import ca.cgagnier.wlednativeandroid.widget.components.WLEDWidgetTheme import kotlinx.serialization.json.Json val WIDGET_DATA_KEY = stringPreferencesKey("widget_data") -private val NARROW_WIDGET_WIDTH_THRESHOLD = 150.dp +private val NARROW_WIDGET_WIDTH_THRESHOLD = 160.dp private val WIDGET_SAFE_PADDING = 12.dp @Composable @@ -69,6 +69,22 @@ fun WidgetContent(context: Context, appWidgetId: Int) { } } +@Suppress("MagicNumber") +@Composable +fun WidgetPreviewContent() { + val previewData = WidgetStateData( + macAddress = "preview", + address = "192.168.1.x", + name = "WLED Device", + isOn = true, + isOnline = true, + color = 0xFF00BFFF.toInt(), // Deep sky blue + ) + WLEDWidgetTheme(previewData) { + DeviceWidgetContentWide(previewData) + } +} + @Composable private fun ErrorState(context: Context, appWidgetId: Int) { // Error State: Make it clickable to open the configuration @@ -108,7 +124,7 @@ private fun DeviceWidgetContent(data: WidgetStateData) { } @Composable -private fun DeviceWidgetContentWide(data: WidgetStateData) { +internal fun DeviceWidgetContentWide(data: WidgetStateData) { DeviceWidgetContainer(data) { Column( modifier = GlanceModifier.fillMaxSize(), @@ -271,22 +287,12 @@ private fun ElapsedTimeChronometerContainer(lastUpdated: Long) { @Preview(widthDp = 200, heightDp = 100) @Composable private fun DeviceWidgetContentPreviewOn() { - val widgetData = WidgetStateData( - macAddress = "AA:BB:CC:DD:EE:FF", - address = "192.168.1.100", - name = "WLED Device", - isOn = true, - isOnline = true, - color = 0xFF0000FF.toInt(), // Blue - ) - WLEDWidgetTheme(widgetData) { - DeviceWidgetContent(widgetData) - } + WidgetPreviewContent() } @Suppress("MagicNumber") @OptIn(ExperimentalGlancePreviewApi::class) -@Preview(widthDp = 100, heightDp = 100) +@Preview(widthDp = 150, heightDp = 100) @Composable private fun DeviceWidgetContentPreviewNarrow() { val widgetData = WidgetStateData( diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WidgetPreviewPublisher.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WidgetPreviewPublisher.kt new file mode 100644 index 0000000..55c7162 --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WidgetPreviewPublisher.kt @@ -0,0 +1,72 @@ +package ca.cgagnier.wlednativeandroid.widget + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.collection.intSetOf +import androidx.core.content.edit +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.GlanceAppWidgetManager.Companion.SET_WIDGET_PREVIEWS_RESULT_SUCCESS +import ca.cgagnier.wlednativeandroid.BuildConfig +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Publishes widget previews for the launcher widget picker (Android 15+). + * Handles rate limiting by checking if preview is already published and tracking app version. + */ +object WidgetPreviewPublisher { + + private const val PREFS_NAME = "widget_preview_prefs" + private const val KEY_PREVIEW_VERSION = "preview_published_version" + + /** + * Publishes widget previews if needed. Should be called during app initialization. + * This is a no-op on Android < 15. + */ + fun publishIfNeeded(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + publishPreviewsInternal(context) + } + } + + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private fun publishPreviewsInternal(context: Context) { + CoroutineScope(Dispatchers.IO).launch { + // Check if preview is already published for home screen category + val appWidgetManager = AppWidgetManager.getInstance(context) + val componentName = ComponentName(context, WledWidgetReceiver::class.java) + val providerInfo = appWidgetManager.getInstalledProvidersForPackage(context.packageName, null) + .firstOrNull { it.provider == componentName } + + val hasHomeScreenPreview = providerInfo?.generatedPreviewCategories + ?.and(AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN) != 0 + + // Also check if app was updated (preview might need refresh) + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val currentVersionCode = BuildConfig.VERSION_CODE + val publishedVersionCode = prefs.getInt(KEY_PREVIEW_VERSION, -1) + val isNewVersion = publishedVersionCode != currentVersionCode + + // Skip if preview exists and we're on the same version + if (hasHomeScreenPreview && !isNewVersion) { + return@launch + } + + val result = GlanceAppWidgetManager(context) + .setWidgetPreviews( + WledWidgetReceiver::class, + intSetOf(AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN), + ) + + // Only save version if successful (not rate-limited) + if (result == SET_WIDGET_PREVIEWS_RESULT_SUCCESS) { + prefs.edit { putInt(KEY_PREVIEW_VERSION, currentVersionCode) } + } + } + } +} diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidget.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidget.kt index a87d64c..d13f756 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidget.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidget.kt @@ -30,6 +30,14 @@ class WledWidget : GlanceAppWidget() { } } + override suspend fun providePreview(context: Context, widgetCategory: Int) { + provideContent { + GlanceTheme { + WidgetPreviewContent() + } + } + } + @EntryPoint @InstallIn(SingletonComponent::class) interface WidgetEntryPoint { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 59430bf..badb022 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -113,7 +113,8 @@ Schließen Einige deiner Geräte sind verborgen - WLED Widget + Einzelgerät + Ein einzelnes WLED-Gerät steuern Bitte konfigurieren Wähle ein Gerät Zuletzt aktualisiert diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fba18c5..03b79ea 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -112,7 +112,8 @@ Fermer Vous avez des appareils cachés - Widget WLED + Appareil unique + Permet de voir et contrôler un seul appareil WLED Veuillez configurer Sélectionnez un appareil Dernière mise à jour diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 9a77721..c118180 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -110,7 +110,8 @@ 关闭 你有已隐藏的设备 - WLED 小部件 + 单个设备 + 控制单个 WLED 设备 请配置 选择设备 最后更新 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 682d3ac..9f15c5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -114,7 +114,8 @@ Dismiss Some of your devices are hidden - WLED Widget + Single Device + Control a single WLED device Please configure Select a Device Last Updated diff --git a/app/src/main/res/xml/wled_widget_info.xml b/app/src/main/res/xml/wled_widget_info.xml index 0d2d623..c0e7914 100644 --- a/app/src/main/res/xml/wled_widget_info.xml +++ b/app/src/main/res/xml/wled_widget_info.xml @@ -2,8 +2,10 @@