Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@

<receiver
android:name=".widget.WledWidgetReceiver"
android:exported="true">
android:exported="true"
android:label="@string/widget_single_device_name">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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) }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ class WledWidget : GlanceAppWidget() {
}
}

override suspend fun providePreview(context: Context, widgetCategory: Int) {
provideContent {
GlanceTheme {
WidgetPreviewContent()
}
}
}

@EntryPoint
@InstallIn(SingletonComponent::class)
interface WidgetEntryPoint {
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@
<string name="dismiss">Schließen</string>
<string name="some_devices_hidden">Einige deiner Geräte sind verborgen</string>
<!-- TODO: Translate the widget texts -->
<string name="widget_description">WLED Widget</string>
<string name="widget_single_device_name">Einzelgerät</string>
<string name="widget_description">Ein einzelnes WLED-Gerät steuern</string>
<string name="widget_please_configure">Bitte konfigurieren</string>
<string name="select_a_device">Wähle ein Gerät</string>
<string name="last_updated">Zuletzt aktualisiert</string>
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@
<string name="dismiss">Fermer</string>
<string name="some_devices_hidden">Vous avez des appareils cachés</string>

<string name="widget_description">Widget WLED</string>
<string name="widget_single_device_name">Appareil unique</string>
<string name="widget_description">Permet de voir et contrôler un seul appareil WLED</string>
<string name="widget_please_configure">Veuillez configurer</string>
<string name="select_a_device">Sélectionnez un appareil</string>
<string name="last_updated">Dernière mise à jour</string>
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values-zh/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@
<string name="dismiss">关闭</string>
<string name="some_devices_hidden">你有已隐藏的设备</string>

<string name="widget_description">WLED 小部件</string>
<string name="widget_single_device_name">单个设备</string>
<string name="widget_description">控制单个 WLED 设备</string>
<string name="widget_please_configure">请配置</string>
<string name="select_a_device">选择设备</string>
<string name="last_updated">最后更新</string>
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@
<string name="dismiss">Dismiss</string>
<string name="some_devices_hidden">Some of your devices are hidden</string>
<!-- TODO: Translate the widget texts -->
<string name="widget_description">WLED Widget</string>
<string name="widget_single_device_name">Single Device</string>
<string name="widget_description">Control a single WLED device</string>
<string name="widget_please_configure">Please configure</string>
<string name="select_a_device">Select a Device</string>
<string name="last_updated">Last Updated</string>
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/res/xml/wled_widget_info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_description"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="110dp"
android:minWidth="70dp"
android:minHeight="40dp"
android:targetCellWidth="3"
android:targetCellHeight="1"
android:resizeMode="horizontal"
android:previewImage="@mipmap/ic_launcher_rgb"
android:updatePeriodMillis="1800000"
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ coreKtx = "1.17.0"
coreSplashscreen = "1.2.0"
datastorePreferences = "1.2.0"
espressoCore = "3.7.0"
glance = "1.1.1"
glance = "1.2.0-rc01"
# Version 2.53.1 causes issue with compiling
# https://github.com/google/dagger/issues/4533
hiltAndroid = "2.57.2"
Expand Down
Loading