2

I'm trying to add an AppWidget to my Android app, built with Kotlin, Jetpack Compose, Room and Dagger-Hilt.

The general idea is for the widget to display the quote of the day, by picking it from the quoteRepository, which returns a Flow<QuoteOfTheDay> from the Room database. In production, the widget should update once a day, ideally at or around midnight in order to reflect the day change and the new quote.

There are two problems I need help with:

  1. When first added to the home screen, the widget picks the correct quote. However, when the app is relaunched in Android Studio, the widget is reset to the default XML layout with dummy data. I would prefer if the current quote persists across app updates and relaunches.
  2. I've done everything in my limited powers to replicate the behaviour described in the official Android widget guide related to layout. Nevertheless, I cannot get the widget to pick the medium-sized layout upon resizing - instead, it just rearranges the small layout.

The widget is using the standard XML layout and this is the provider:


@AndroidEntryPoint
class AppWidget : AppWidgetProvider() {

    private val job = SupervisorJob()
    private val coroutineScope = CoroutineScope(Dispatchers.Default + job)
    @Inject lateinit var repository: quoteRepository
    private val today = LocalDate.now()
    private val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("MMMM")

    private val supportedSizes = listOf(
        SizeFCompat(160.0f, 110.0f),
        SizeFCompat(200.0f, 110.0f)
    )

    override fun onReceive(context: Context, intent: Intent?) {
        super.onReceive(context, intent)
        var layoutId = 0

        coroutineScope.launch {

            repository.getDailyquote(today).collect { _quote ->
                val appWidgetManager = AppWidgetManager.getInstance(context)
                val ids = appWidgetManager.getAppWidgetIds(ComponentName(context, AppWidget::class.java))

                for (appWidgetId in ids) {

                    appWidgetManager.updateAppWidget(appWidgetId, supportedSizes) {
                        layoutId = when (it) {
                            supportedSizes[0] -> R.layout.quote_widget_small
                            else -> R.layout.quote_widget_medium
                        }

                        RemoteViews(context.packageName, layoutId)
                    }

                    updateWidget(
                        context, appWidgetManager, appWidgetId, layoutId,
                        _quote.quote.title,
                        today.dayOfMonth.toString(),
                        today.format(dateFormatter)
                    )
                }
            }
        }
    }

    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {

        var layoutId = 0

        coroutineScope.launch {

            for (id in appWidgetIds) {

                appWidgetManager.updateAppWidget(id, supportedSizes) {
                    layoutId = when (it) {
                        supportedSizes[0] -> R.layout.quote_widget_small
                        else -> R.layout.quote_widget_medium
                    }

                    RemoteViews(context.packageName, layoutId)
                }

                repository.getDailyquote(today).collect { a ->
                    updateWidget(
                        context,
                        appWidgetManager,
                        id,
                        layoutId,
                        a.quote.title,
                        today.dayOfMonth.toString(),
                        today.format(dateFormatter)
                    )
                }
            }

        }

        scheduleUpdates(context)
    }

    override fun onDisabled(context: Context) {
        job.cancel()
    }

    override fun onDeleted(context: Context, appWidgetIds: IntArray) {
        // reschedule update alarm so it does not include ID of currently removed widget
        scheduleUpdates(context)
    }

    private fun getActiveWidgetIds(context: Context): IntArray {
        val appWidgetManager = AppWidgetManager.getInstance(context)
        val componentName = ComponentName(context, this::class.java)

        // return ID of all active widgets within this AppWidgetProvider
        return appWidgetManager.getAppWidgetIds(componentName)
    }

    private fun scheduleUpdates(context: Context) {
        val activeWidgetIds = getActiveWidgetIds(context)

        if (activeWidgetIds.isNotEmpty()) {
            val nextUpdate = ZonedDateTime.now() + WIDGET_UPDATE_INTERVAL
            val pendingIntent = getUpdatePendingIntent(context)

            context.alarmManager.set(
                AlarmManager.RTC_WAKEUP,
                nextUpdate.toInstant().toEpochMilli(), // alarm time in millis since 1970-01-01 UTC
                pendingIntent
            )
        }
    }

    private fun getUpdatePendingIntent(context: Context): PendingIntent {
        val widgetClass = this::class.java
        val widgetIds = getActiveWidgetIds(context)
        val updateIntent = Intent(context, widgetClass)
            .setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
            .putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds)
        val requestCode = widgetClass.name.hashCode()
        val flags = PendingIntent.FLAG_CANCEL_CURRENT or
                PendingIntent.FLAG_IMMUTABLE

        return PendingIntent.getBroadcast(context, requestCode, updateIntent, flags)
    }

    private val Context.alarmManager: AlarmManager
        get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager

    companion object {
        private val WIDGET_UPDATE_INTERVAL = Duration.ofMinutes(2)
    }

    private fun updateWidget(
        context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, layoutId: Int,
        title: String?, day: String?, month: String?
    ) {
        // Construct the RemoteViews object
        val views = RemoteViews(context.packageName, layoutId)

        if (title != null) {
            views.setTextViewText(R.id.quote_text, title)
        } else {
            views.setTextViewText(R.id.quote_text, "")
        }
        if (day != null) {
            views.setTextViewText(R.id.date_text, day)
        } else {
            views.setTextViewText(R.id.date_text, "")
        }
        if (month != null) {
            views.setTextViewText(R.id.month_text, month.lowercase())
        } else {
            views.setTextViewText(R.id.month_text, "")
        }

        views.setOnClickPendingIntent(R.id.widget_layout,
            getPendingIntentActivity(context))

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views)
    }

    private fun getPendingIntentActivity(context: Context): PendingIntent {
        // Construct an Intent which is pointing this class.
        val intent = Intent(context, MainActivity::class.java)
        // And this time we are sending a broadcast with getBroadcast
        return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
    }
}
zkvvoob
  • 394
  • 1
  • 4
  • 26
  • zkvvoob Did you find anything regarding your first issue? I'm also having the same problem. @zkvvoob – tzegian Jul 13 '23 at 21:25

0 Answers0