Skip to content

Commit 0f404de

Browse files
committed
Refactor Full Bleed App Widget for safe execution and graceful degradation
1 parent fbc7f4d commit 0f404de

4 files changed

Lines changed: 96 additions & 50 deletions

File tree

samples/user-interface/appwidgets/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ android {
2626
compileSdk = 37
2727

2828
defaultConfig {
29-
minSdk = 21
29+
minSdk = 23
3030
}
3131

3232
compileOptions {

samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/CanonicalLayoutActivity.kt

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -90,35 +90,36 @@ class CanonicalLayoutActivity : ComponentActivity() {
9090
}
9191
}
9292

93-
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
9493
override fun onCreate(savedInstanceState: Bundle?) {
9594
enableEdgeToEdge()
9695

9796
super.onCreate(savedInstanceState)
9897

9998
// Publish Generated Widget Previews
100-
lifecycleScope.launch {
101-
try {
102-
val context = this@CanonicalLayoutActivity
103-
val receiver = FullBleedImageAppWidgetReceiver::class.java
104-
val glanceAppWidgetManager = GlanceAppWidgetManager(context)
105-
val appWidgetManager = context.getSystemService(AppWidgetManager::class.java)
106-
107-
val providerInfo = appWidgetManager.installedProviders.firstOrNull {
108-
it.provider.className == receiver.name
109-
}
99+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
100+
lifecycleScope.launch {
101+
try {
102+
val context = this@CanonicalLayoutActivity
103+
val receiver = FullBleedImageAppWidgetReceiver::class.java
104+
val glanceAppWidgetManager = GlanceAppWidgetManager(context)
105+
val appWidgetManager = context.getSystemService(AppWidgetManager::class.java)
106+
107+
val providerInfo = appWidgetManager?.installedProviders?.firstOrNull {
108+
it.provider.className == receiver.name
109+
}
110110

111-
if (providerInfo?.generatedPreviewCategories == 0) {
112-
val result = glanceAppWidgetManager.setWidgetPreviews(FullBleedImageAppWidgetReceiver::class)
113-
val status = when (result) {
114-
GlanceAppWidgetManager.SET_WIDGET_PREVIEWS_RESULT_SUCCESS -> "Success"
115-
GlanceAppWidgetManager.SET_WIDGET_PREVIEWS_RESULT_RATE_LIMITED -> "Rate-Limited"
116-
else -> "Error ($result)"
111+
if (providerInfo?.generatedPreviewCategories == 0) {
112+
val result = glanceAppWidgetManager.setWidgetPreviews(FullBleedImageAppWidgetReceiver::class)
113+
val status = when (result) {
114+
GlanceAppWidgetManager.SET_WIDGET_PREVIEWS_RESULT_SUCCESS -> "Success"
115+
GlanceAppWidgetManager.SET_WIDGET_PREVIEWS_RESULT_RATE_LIMITED -> "Rate-Limited"
116+
else -> "Error ($result)"
117+
}
118+
Log.i("CanonicalLayoutActivity", "Published previews for ${receiver.simpleName}: $status")
117119
}
118-
Log.i("CanonicalLayoutActivity", "Published previews for ${receiver.simpleName}: $status")
120+
} catch (e: Exception) {
121+
Log.e("CanonicalLayoutActivity", "Failed to set widget previews", e)
119122
}
120-
} catch (e: Exception) {
121-
Log.e("CanonicalLayoutActivity", "Failed to set widget previews", e)
122123
}
123124
}
124125

samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/text/FullBleedImageAppWidget.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.example.platform.ui.appwidgets.glance.layout.text
1717

1818
import android.content.Context
19+
import android.util.Log
1920
import android.os.Build
2021
import androidx.annotation.RequiresApi
2122
import androidx.compose.runtime.collectAsState
@@ -47,7 +48,6 @@ class FullBleedImageAppWidget : GlanceAppWidget() {
4748
)
4849
)
4950

50-
@RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1)
5151
override suspend fun provideGlance(context: Context, id: GlanceId) {
5252
val repo = getImageGridDataRepo(id)
5353

@@ -66,7 +66,6 @@ class FullBleedImageAppWidget : GlanceAppWidget() {
6666
}
6767
}
6868

69-
@RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1)
7069
override suspend fun providePreview(context: Context, widgetCategory: Int) {
7170
val repo = FakeImageGridDataRepository()
7271

@@ -92,9 +91,13 @@ class FullBleedImageAppWidgetReceiver : GlanceAppWidgetReceiver() {
9291

9392
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
9493
val glanceAppWidgetManager = GlanceAppWidgetManager(context)
95-
appWidgetIds.forEach {
96-
val glanceId = glanceAppWidgetManager.getGlanceIdBy(it)
97-
FakeImageGridDataRepository.cleanUp(glanceId)
94+
appWidgetIds.forEach { id ->
95+
try {
96+
val glanceId = glanceAppWidgetManager.getGlanceIdBy(id)
97+
FakeImageGridDataRepository.cleanUp(glanceId)
98+
} catch (e: IllegalArgumentException) {
99+
Log.w("FullBleedImageReceiver", "Skipping cleanup for invalid AppWidget ID: $id", e)
100+
}
98101
}
99102
super.onDeleted(context, appWidgetIds)
100103
}

samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/text/layout/FullBleedImageLayout.kt

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.annotation.RequiresApi
2121
import androidx.compose.runtime.Composable
2222
import androidx.compose.ui.graphics.Color
2323
import androidx.compose.ui.unit.dp
24+
import androidx.compose.ui.unit.DpSize
2425
import androidx.glance.GlanceModifier
2526
import androidx.glance.Image
2627
import androidx.glance.ImageProvider
@@ -58,7 +59,6 @@ import com.example.platform.ui.appwidgets.glance.layout.utils.SmallWidgetPreview
5859
* Each item displays an edge-to-edge background photo with overlaid
5960
* title and caption details that auto-scale to fit the current widget size.
6061
*/
61-
@RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1)
6262
@Composable
6363
fun FullBleedImageLayout(
6464
data: List<ImageGridItemData>? = null,
@@ -81,30 +81,74 @@ fun FullBleedImageLayout(
8181
actionButtonIcon = R.drawable.sample_info_icon,
8282
actionButtonOnClick = actionStartDemoActivity("on-click of info button in no data view")
8383
)
84-
} else if (data.size == 1) {
85-
// If there's only 1 item (like in the widget preview), render with fillMaxSize to
86-
// bypass LazyColumn measurement issues where the generated widget preview item doesn't
87-
// fill the widget bounds.
88-
GalleryItemCard(
89-
item = data[0],
84+
} else if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.BAKLAVA_1) {
85+
SnapScrollingGallery(
86+
data = data,
9087
isSmall = isSmall,
9188
appName = appName,
92-
modifier = GlanceModifier.fillMaxSize()
89+
size = size
9390
)
9491
} else {
95-
val limitedData = data.take(5)
96-
LazyColumn(
97-
modifier = GlanceModifier.fillMaxSize(),
98-
verticalScrollMode = VerticalScrollMode.SnapScrollMatchHeight(size.height)
99-
) {
100-
items(limitedData, itemId = { it.key.hashCode().toLong() }) { item ->
101-
GalleryItemCard(
102-
item = item,
103-
isSmall = isSmall,
104-
appName = appName,
105-
modifier = GlanceModifier.width(size.width).height(size.height)
106-
)
107-
}
92+
// Show a standard scrolling list of items without Snap Scrolling
93+
// TODO: Remove once Snap Scrolling gracefully degrades
94+
GalleryList(
95+
data = data,
96+
isSmall = isSmall,
97+
appName = appName,
98+
size = size
99+
)
100+
}
101+
}
102+
}
103+
104+
@RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1)
105+
@Composable
106+
private fun SnapScrollingGallery(
107+
data: List<ImageGridItemData>,
108+
isSmall: Boolean,
109+
appName: String,
110+
size: DpSize,
111+
) {
112+
GalleryList(
113+
data = data,
114+
isSmall = isSmall,
115+
appName = appName,
116+
size = size,
117+
verticalScrollMode = VerticalScrollMode.SnapScrollMatchHeight(size.height)
118+
)
119+
}
120+
121+
@Composable
122+
private fun GalleryList(
123+
data: List<ImageGridItemData>,
124+
isSmall: Boolean,
125+
appName: String,
126+
size: DpSize,
127+
verticalScrollMode: VerticalScrollMode = VerticalScrollMode.Normal
128+
) {
129+
if (data.size == 1) {
130+
// If there's only 1 item (like in the widget preview), render with fillMaxSize to
131+
// bypass LazyColumn measurement issues where the generated widget preview item doesn't
132+
// fill the widget bounds.
133+
GalleryItemCard(
134+
item = data[0],
135+
isSmall = isSmall,
136+
appName = appName,
137+
modifier = GlanceModifier.fillMaxSize()
138+
)
139+
} else {
140+
val limitedData = data.take(5)
141+
LazyColumn(
142+
modifier = GlanceModifier.fillMaxSize(),
143+
verticalScrollMode = verticalScrollMode
144+
) {
145+
items(limitedData, itemId = { item -> item.key.hashCode().toLong() }) { item ->
146+
GalleryItemCard(
147+
item = item,
148+
isSmall = isSmall,
149+
appName = appName,
150+
modifier = GlanceModifier.width(size.width).height(size.height)
151+
)
108152
}
109153
}
110154
}
@@ -142,9 +186,7 @@ private fun GalleryItemCard(
142186
modifier = GlanceModifier
143187
.fillMaxWidth()
144188
// Implementing a partial gradient scrim by applying a background modifier directly
145-
// to a text Column, shown here, results in the gradient stretching to fill the
146-
// entire widget. You can work around this by using an independent sibling Image
147-
// with a hardcoded height (100.dp) to restrict a gradient to the bottom.
189+
// to a text Column results in the gradient stretching to fill the entire widget.
148190
.background(ImageProvider(R.drawable.sample_scrim_gradient))
149191
.padding(WidgetTextDimensions.widgetPadding),
150192
verticalAlignment = Alignment.Bottom,

0 commit comments

Comments
 (0)