Loading...
Loading...
Manages Android AppWidget state updates, background syncs, and RemoteViews bitmap memory limits with Jetpack Glance (androidx.glance:glance-appwidget) and AppWidgetProvider receivers on Android 17 (API 37). Use this skill when configuring Android 17 (API level 37) compileSdk/targetSdk and AndroidManifest.xml widget receivers, refreshing frozen or stale Glance widget UI after CoroutineWorker DataStore preference updates (updateAppWidgetState), downsampling images within the WindowMetrics memory limit for SizeMode.Responsive, SizeMode.Exact, or LazyColumn, sharing content:// URIs with FileProvider, or fixing ActionCallback NoSuchMethodException and AppWidgetProvider ForegroundServiceStartNotAllowedException crashes with WorkManager. Don't use for Glance layout templates (suggest the glance-canonical-layouts skill) or in-app Compose screens.
npx skill4agent add android/skills glance-appwidget-updates-and-memoryandroidx.glance:glance-appwidgetRemoteViewsglance-canonical-layoutstargetSdk = 37AppWidgetProviderminSdk >= 26compileSdk = 37targetSdk = 37build.gradle.kts1.2.01.1.1+build.gradle.ktsgradle/libs.versions.tomlnamespaceapplicationIdpackagetargetSdksystem_serverRemoteViews6 * width * height1.5 * 4 * width * heightQHD+maxWidthDp x maxHeightDpRGB_565ALPHA_8calculateWidgetMemoryCapBytes(context) / distinctImageCount6 * width * heightWindowManager.currentWindowMetrics.boundsresources.displayMetricsWindowManagerSizeMode.Exactglance-canonical-layoutsSizeMode.ResponsiveDpSizeprovideContentprovideGlanceprovideContentImageProvider(bitmap)RemoteViewsBitmapLocalSize.currentcalculateWidgetMemoryCapBytes(context)targetSdk >= 37IconBitmapcontent://ImageProvider(uri)RemoteViews.setImageViewUricontext.cacheDirFileProvider.getUriForFileIntent.FLAG_GRANT_READ_URI_PERMISSIONIntent.CATEGORY_HOMEupdateAppWidgetStateGlanceAppWidget.update(context, glanceId)GlanceAppWidget.updateAll(context)AppWidgetProviderIconAppWidgetManager.updateAppWidgetpartiallyUpdateAppWidgetreferences/packagegradle/libs.versions.tomlapp/build.gradle.ktsapp/src/main/AndroidManifest.xmlglance = "1.2.0"androidx.glance:glance-appwidgetandroidx.glance:glance-material3gradle/libs.versions.tomlapp/build.gradle.ktsnamespaceapplicationIdminSdk >= 26targetSdkcompileSdk = 37targetSdk = 37buildFeatures { compose = true }implementation(libs.androidx.glance.appwidget)implementation(libs.androidx.glance.material3)AndroidManifest.xml<queries><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.HOME" />queryIntentActivitiesGlanceAppWidgetReceiverandroid.appwidget.provider@xml/sample_widget_infoandroidx.core.content.FileProviderandroid:authorities="${applicationId}.fileprovider"android:exported="false"android:grantUriPermissions="true"android.support.FILE_PROVIDER_PATHS@xml/file_paths<cache-path name="widget_cache" path="widget_images/" /><receiver android:name=".glance.MyReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/my_app_widget_info" />
</receiver>calculateWidgetMemoryCapBytesBuild.VERSION.SDK_INT >= Build.VERSION_CODES.Rval bounds = context.getSystemService(WindowManager::class.java)?.currentWindowMetrics?.boundsbounds != null && bounds.width() > 0 && bounds.height() > 06L * bounds.width().toLong() * bounds.height().toLong()WindowManagerContextStrictModeWindowManagercontext.resources.displayMetrics6L * dm.widthPixels.toLong() * dm.heightPixels.toLong()system_server6 * width * heightmaxWidthDp x maxHeightDpdecodeDownsampledWidgetBitmapBitmapFactory.Options().apply { inJustDecodeBounds = true; inPreferredConfig = config }sampleSize *= 2(bounds.outWidth / (sampleSize * 2) >= targetWidthPx && bounds.outHeight / (sampleSize * 2) >= targetHeightPx) || (bounds.outWidth / sampleSize > targetWidthPx * 2) || (bounds.outHeight / sampleSize > targetHeightPx * 2)targetWidthPx = max(1, (maxWidthDp * density).toInt())perImageCapBytes = calculateWidgetMemoryCapBytes(context) / max(1, distinctImageCount)sampleSize *= 2(max(1, bounds.outWidth / sampleSize).toLong() * max(1, bounds.outHeight / sampleSize).toLong() * bytesPerPixel) > perImageCapBytes2LBitmap.Config.RGB_5651LBitmap.Config.ALPHA_84Lbounds.outConfig != null && bounds.outConfig != configBitmapFactoryRGB_565ARGB_8888inSampleSize = sampleSizeinPreferredConfig = configBitmapFileProviderUricacheWidgetImageUridecodeDownsampledWidgetBitmapandroidx.glance.ImageProvider(bitmap)cacheWidgetImageUricontent://UriImageProvider(uri)RemoteViews.setImageViewUriFile(context.cacheDir, "widget_images")content://FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", outFile)Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)context.grantUriPermission(pkg, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)SizeModeSizeMode.Exactglance-canonical-layoutsLocalSize.currentSizeMode.Responsive(RESPONSIVE_BUCKETS)import androidx.compose.ui.unit.DpSizeimport androidx.compose.ui.unit.dpDpSizesetOf(DpSize(120.dp, 120.dp), DpSize(240.dp, 120.dp), DpSize(240.dp, 240.dp))SizeMode.SingleprovideContentSizeMode.Exact2-4SizeMode.ResponsiveNprovideContentprovideGlanceprovideContentdecodeDownsampledWidgetBitmapBitmap.Config.RGB_565distinctImageCount = 1Bitmapandroidx.glance.ImageProvider(sharedBitmap)RemoteViewsBitmaponCompositionError(context, glanceId, appWidgetId, throwable)AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, fallback)RemoteViews0LazyColumnimport androidx.glance.appwidget.ImageProviderUriimport androidx.glance.ImageProviderBitmapUri.fromFileIcon.createWithContentUri.update().updateAll()provideGlancevar cachedRevision = -1var cachedThumbnails = emptyMap<String, Bitmap>()provideGlanceprovideContentprovideContentval revision = currentState<Preferences>()[KEY_REVISION] ?: 0revision != cachedRevisioncachedRevision = revisionfeedItemscachedThumbnails = feedItems.mapNotNull { item -> decodeDownsampledWidgetBitmap(..., distinctImageCount = max(1, feedItems.size), config = Bitmap.Config.RGB_565)?.let { item.id to it } }.toMap()mapNotNullassociateMap<String, Bitmap>Map<String, Bitmap?>Map<String, Bitmap>SizeMode.ResponsiveLazyColumnLongitemIditems(items = feedItems, itemId = { item -> item.id.hashCode().toLong() })Image(provider = ImageProvider(bitmap), contentDescription = item.title)bitmap != nullUri.fromFileelseandroidx.datastore:datastore-preferences-core:1.2.1import androidx.glance.appwidget.GlanceAppWidgetManagerimport androidx.glance.appwidget.state.updateAppWidgetStateimport androidx.glance.appwidget.updateAllpushWidgetRevisionUpdate(context)for (glanceId in GlanceAppWidgetManager(context).getGlanceIds(Widget::class.java))updateAppWidgetState(context, glanceId) { prefs -> ... }contextglanceIdwidget.update(context, glanceId)widget.updateAll(context)ActionCallbackActionParameters.Key<init>()androidx.work:work-runtime-ktx:2.12.0RunCallbackActionActionCallbackClass.getDeclaredConstructor().newInstance()ActionCallbackActionParameters.KeyactionParametersOfRefreshWidgetAction-keep class * implements androidx.glance.appwidget.action.ActionCallback { public <init>(); }proguard-rules.proSyncWidgetWorker.doWork()pushWidgetRevisionUpdate(applicationContext)WorkManagerAppWidgetProvider.onUpdateupdateAppWidgetcontext.startForegroundServiceAppWidgetProvider.onReceiveonUpdateForegroundServiceStartNotAllowedExceptionGlanceAppWidgetReceiverRescheduleReceiverAppWidgetProviderOneTimeWorkRequestonUpdateExistingWorkPolicy.KEEPSUCCEEDEDUnfinishedWorkListenerRescheduleReceiverACTION_PACKAGE_CHANGEDonUpdateonUpdateenqueueUniquePeriodicWork(..., ExistingPeriodicWorkPolicy.KEEP, ...)ENQUEUEDgoAsync()< 10senqueueUniqueWork(..., ExistingWorkPolicy.KEEP, ...)onReceiveBitmapIconRemoteViewsappWidgetManager.updateAppWidgetpartiallyUpdateAppWidget| Symptom or exception | Root cause | Fix |
|---|---|---|
| | Downsample to the widget's |
| Home launcher lacks read permission for the | Expose the file with |
Widget UI stays frozen after | | Call |
| | Use a public zero-argument constructor, pass values with |
| | Schedule periodic syncs in |
| Missing or mismatched Glance and Compose package imports, or calling | Import |
BitmapprovideContentSizeMode.ResponsiveSizeMode.Exact6 * width * heightmaxWidthDp x maxHeightDpcontent://UriUri.fromFileImageProvider(uri)RemoteViews.setImageViewUricontext.grantUriPermissionIntent.CATEGORY_HOMEupdateAppWidgetStateGlanceAppWidget.update(context, glanceId)GlanceAppWidget.updateAll(context)partiallyUpdateAppWidget