GlanceAppWidgetReceiver.kt

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.glance.appwidget

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.annotation.CallSuper
import androidx.glance.ExperimentalGlanceApi
import androidx.glance.appwidget.action.LambdaActionBroadcasts
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch

/**
 * [AppWidgetProvider] using the given [GlanceAppWidget] to generate the remote views when needed.
 *
 * This should typically used as:
 *
 *     class MyGlanceAppWidgetProvider : GlanceAppWidgetProvider() {
 *       override val glanceAppWidget: GlanceAppWidget()
 *         get() = MyGlanceAppWidget()
 *     }
 *
 * Note: If you override any of the [AppWidgetProvider] methods, ensure you call their super-class
 * implementation.
 *
 * Important: if you override any of the methods of this class, you must call the super
 * implementation, and you must not call [AppWidgetProvider.goAsync], as it will be called by the
 * super implementation. This means your processing time must be short.
 */
@OptIn(ExperimentalGlanceApi::class)
abstract class GlanceAppWidgetReceiver : AppWidgetProvider() {

    companion object {
        private const val TAG = "GlanceAppWidgetReceiver"

        /**
         * Action for a broadcast intent that will try to update all instances of a Glance App
         * Widget for debugging.
         * <pre>
         * adb shell am broadcast -a androidx.glance.appwidget.action.DEBUG_UPDATE -n APP/COMPONENT
         * </pre>
         * where APP/COMPONENT is the manifest component for the GlanceAppWidgetReceiver subclass.
         * This only works if the Receiver is exported (or the target device has adb running as
         * root), and has androidx.glance.appwidget.DEBUG_UPDATE in its intent-filter.
         * This should only be done for debug builds and disabled for release.
         */
        const val ACTION_DEBUG_UPDATE = "androidx.glance.appwidget.action.DEBUG_UPDATE"
    }

    /**
     * Instance of the [GlanceAppWidget] to use to generate the App Widget and send it to the
     * [AppWidgetManager]
     */
    abstract val glanceAppWidget: GlanceAppWidget

    /**
     * Override [coroutineContext] to provide custom [CoroutineContext] in which to run
     * update requests.
     *
     * Note: This does not set the [CoroutineContext] for the GlanceAppWidget, which will always run
     * on the main thread.
     */
    @get:ExperimentalGlanceApi
    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
    @ExperimentalGlanceApi
    open val coroutineContext: CoroutineContext = Dispatchers.Default

    @CallSuper
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            Log.w(
                TAG,
                "Using Glance in devices with API<23 is untested and might behave unexpectedly."
            )
        }
        goAsync(coroutineContext) {
            updateManager(context)
            appWidgetIds.map { async { glanceAppWidget.update(context, it) } }
                .awaitAll()
        }
    }

    @CallSuper
    override fun onAppWidgetOptionsChanged(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetId: Int,
        newOptions: Bundle
    ) {
        goAsync(coroutineContext) {
            updateManager(context)
            glanceAppWidget.resize(context, appWidgetId, newOptions)
        }
    }

    @CallSuper
    override fun onDeleted(context: Context, appWidgetIds: IntArray) {
        goAsync(coroutineContext) {
            updateManager(context)
            appWidgetIds.forEach { glanceAppWidget.deleted(context, it) }
        }
    }

    private fun CoroutineScope.updateManager(context: Context) {
        launch {
            runAndLogExceptions {
                GlanceAppWidgetManager(context)
                    .updateReceiver(this@GlanceAppWidgetReceiver, glanceAppWidget)
            }
        }
    }

    override fun onReceive(context: Context, intent: Intent) {
        runAndLogExceptions {
            when (intent.action) {
                Intent.ACTION_LOCALE_CHANGED, ACTION_DEBUG_UPDATE -> {
                    val appWidgetManager = AppWidgetManager.getInstance(context)
                    val componentName = ComponentName(
                        context.packageName,
                        checkNotNull(javaClass.canonicalName) { "no canonical name" }
                    )
                    val ids = if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)) {
                        intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)!!
                    } else {
                        appWidgetManager.getAppWidgetIds(componentName)
                    }
                    onUpdate(
                        context,
                        appWidgetManager,
                        ids,
                    )
                }
                LambdaActionBroadcasts.ActionTriggerLambda -> {
                    val actionKey = intent.getStringExtra(LambdaActionBroadcasts.ExtraActionKey)
                            ?: error("Intent is missing ActionKey extra")
                    val id = intent.getIntExtra(LambdaActionBroadcasts.ExtraAppWidgetId, -1)
                    if (id == -1) error("Intent is missing AppWidgetId extra")
                    goAsync(coroutineContext) {
                        updateManager(context)
                        glanceAppWidget.triggerAction(context, id, actionKey)
                    }
                }
                else -> super.onReceive(context, intent)
            }
        }
    }
}

private inline fun runAndLogExceptions(block: () -> Unit) {
    try {
        block()
    } catch (ex: CancellationException) {
        // Nothing to do
    } catch (throwable: Throwable) {
        logException(throwable)
    }
}