1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.glance.appwidget.preview
18 
19 import android.appwidget.AppWidgetHostView
20 import android.content.Context
21 import android.util.AttributeSet
22 import androidx.compose.runtime.Composable
23 import androidx.compose.runtime.currentComposer
24 import androidx.compose.ui.unit.Dp
25 import androidx.compose.ui.unit.DpSize
26 import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
27 import androidx.glance.appwidget.GlanceRemoteViews
28 import androidx.glance.appwidget.preview.ComposableInvoker.invokeComposable
29 import kotlinx.coroutines.runBlocking
30 
31 private const val TOOLS_NS_URI = "http://schemas.android.com/tools"
32 private const val ANDROID_NS_URI = "http://schemas.android.com/apk/res/android"
33 
34 /**
35  * View adapter that renders a glance `@Composable`. The `@Composable` is found by reading the
36  * `tools:composableName` attribute that contains the FQN of the function.
37  */
38 internal class GlanceAppWidgetViewAdapter : AppWidgetHostView {
39 
40     constructor(context: Context, attrs: AttributeSet) : super(context) {
41         init(attrs)
42     }
43 
44     constructor(
45         context: Context,
46         attrs: AttributeSet,
47         @Suppress("UNUSED_PARAMETER") defStyleAttr: Int
48     ) : super(context) {
49         init(attrs)
50     }
51 
52     @OptIn(ExperimentalGlanceRemoteViewsApi::class)
initnull53     internal fun init(
54         className: String,
55         methodName: String,
56         size: DpSize,
57     ) {
58         val content =
59             @Composable {
60                 val composer = currentComposer
61                 invokeComposable(className, methodName, composer)
62             }
63 
64         val remoteViews = runBlocking {
65             GlanceRemoteViews()
66                 .compose(context = context, size = size, content = content)
67                 .remoteViews
68         }
69         val view = remoteViews.apply(context, this)
70         addView(view)
71     }
72 
initnull73     private fun init(attrs: AttributeSet) {
74         val composableName = attrs.getAttributeValue(TOOLS_NS_URI, "composableName") ?: return
75         val className = composableName.substringBeforeLast('.')
76         val methodName = composableName.substringAfterLast('.')
77 
78         val width =
79             attrs
80                 .getAttributeValue(ANDROID_NS_URI, "layout_width")
81                 ?.removeSuffix("dp")
82                 ?.toFloatOrNull()
83         val height =
84             attrs
85                 .getAttributeValue(ANDROID_NS_URI, "layout_height")
86                 ?.removeSuffix("dp")
87                 ?.toFloatOrNull()
88         var size = DpSize.Unspecified
89         if (width != null && height != null) size = DpSize(Dp(width), Dp(height))
90 
91         init(className, methodName, size)
92     }
93 }
94