1 /*
<lambda>null2  * Copyright 2021 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.demos
18 
19 import android.content.ContentProvider
20 import android.content.ContentValues
21 import android.content.Context
22 import android.database.Cursor
23 import android.graphics.Bitmap
24 import android.graphics.Canvas
25 import android.graphics.Paint
26 import android.graphics.drawable.Icon
27 import android.net.Uri
28 import android.os.Build
29 import android.os.ParcelFileDescriptor
30 import androidx.compose.runtime.Composable
31 import androidx.compose.runtime.getValue
32 import androidx.compose.runtime.mutableStateOf
33 import androidx.compose.runtime.remember
34 import androidx.compose.runtime.setValue
35 import androidx.compose.ui.graphics.Color
36 import androidx.compose.ui.graphics.toArgb
37 import androidx.compose.ui.unit.dp
38 import androidx.glance.Button
39 import androidx.glance.ColorFilter
40 import androidx.glance.GlanceId
41 import androidx.glance.GlanceModifier
42 import androidx.glance.Image
43 import androidx.glance.ImageProvider
44 import androidx.glance.LocalContext
45 import androidx.glance.action.clickable
46 import androidx.glance.appwidget.GlanceAppWidget
47 import androidx.glance.appwidget.GlanceAppWidgetReceiver
48 import androidx.glance.appwidget.ImageProvider
49 import androidx.glance.appwidget.SizeMode
50 import androidx.glance.appwidget.components.Scaffold
51 import androidx.glance.appwidget.demos.ShareableImageUtils.getShareableImageUri
52 import androidx.glance.appwidget.demos.ShareableImageUtils.uriImageFile
53 import androidx.glance.appwidget.lazy.LazyColumn
54 import androidx.glance.appwidget.provideContent
55 import androidx.glance.background
56 import androidx.glance.color.ColorProvider
57 import androidx.glance.layout.Alignment
58 import androidx.glance.layout.Box
59 import androidx.glance.layout.Column
60 import androidx.glance.layout.ContentScale
61 import androidx.glance.layout.Row
62 import androidx.glance.layout.Spacer
63 import androidx.glance.layout.fillMaxSize
64 import androidx.glance.layout.fillMaxWidth
65 import androidx.glance.layout.padding
66 import androidx.glance.layout.size
67 import androidx.glance.text.Text
68 import java.io.File
69 
70 class ImageAppWidgetReceiver : GlanceAppWidgetReceiver() {
71     override val glanceAppWidget: GlanceAppWidget = ImageAppWidget()
72 }
73 
74 /** Sample AppWidget that showcase the [ContentScale] options for [Image] */
75 class ImageAppWidget : GlanceAppWidget() {
76     override val sizeMode: SizeMode = SizeMode.Exact
77 
provideGlancenull78     override suspend fun provideGlance(context: Context, id: GlanceId) {
79         val imageUri: Uri = getShareableImageUri(context)
80 
81         provideContent {
82             Scaffold(titleBar = { Header() }, content = { BodyContent(imageUri = imageUri) })
83         }
84     }
85 
providePreviewnull86     override suspend fun providePreview(context: Context, widgetCategory: Int) {
87         val imageUri: Uri = getShareableImageUri(context)
88 
89         provideContent {
90             Scaffold(titleBar = { Header() }, content = { BodyContent(imageUri = imageUri) })
91         }
92     }
93 }
94 
95 @Composable
Headernull96 private fun Header() {
97     val context = LocalContext.current
98     var shouldTintHeaderIcon by remember { mutableStateOf(true) }
99 
100     Row(
101         horizontalAlignment = Alignment.CenterHorizontally,
102         verticalAlignment = Alignment.CenterVertically,
103         modifier = GlanceModifier.fillMaxWidth().background(Color.White)
104     ) {
105         // Demonstrates toggling application of color filter on an image
106         Image(
107             provider = ImageProvider(R.drawable.ic_android),
108             contentDescription = null,
109             colorFilter =
110                 if (shouldTintHeaderIcon) {
111                     ColorFilter.tint(ColorProvider(day = Color.Green, night = Color.Blue))
112                 } else {
113                     null
114                 },
115             modifier = GlanceModifier.clickable { shouldTintHeaderIcon = !shouldTintHeaderIcon }
116         )
117         Text(
118             text = context.getString(R.string.image_widget_name),
119             modifier = GlanceModifier.padding(8.dp),
120         )
121     }
122 }
123 
124 @Composable
BodyContentnull125 private fun BodyContent(imageUri: Uri) {
126     var type by remember { mutableStateOf(ContentScale.Fit) }
127     Column(modifier = GlanceModifier.fillMaxSize().padding(8.dp)) {
128         Spacer(GlanceModifier.size(4.dp))
129         Button(
130             text = "Content Scale: ${type.asString()}",
131             modifier = GlanceModifier.fillMaxWidth(),
132             onClick = {
133                 type =
134                     when (type) {
135                         ContentScale.Crop -> ContentScale.FillBounds
136                         ContentScale.FillBounds -> ContentScale.Fit
137                         else -> ContentScale.Crop
138                     }
139             }
140         )
141         Spacer(GlanceModifier.size(4.dp))
142 
143         val itemModifier = GlanceModifier.fillMaxWidth().padding(vertical = 4.dp)
144 
145         LazyColumn(GlanceModifier.fillMaxSize()) {
146             val textModifier = GlanceModifier.padding(bottom = 8.dp)
147             item {
148                 Column {
149                     // An image who's provider uses a resource.
150                     ResourceImage(contentScale = type, modifier = itemModifier)
151                     Text("ImageProvider(resourceId)", textModifier)
152                 }
153             }
154             item {
155                 Column {
156                     // An image who's provider uses a content uri. This uri will be passed through
157                     // the remote views to the AppWidget's host. The host will query our app via
158                     // content provider to resolve the Uri into a bitmap.
159                     UriImage(uri = imageUri, contentScale = type, modifier = itemModifier)
160                     Text("ImageProvider(uri)", textModifier)
161                 }
162             }
163 
164             item {
165                 Column {
166                     // An image who's provider holds an in-memory bitmap.
167                     BitmapImage(contentScale = type, modifier = itemModifier)
168                     Text("ImageProvider(bitmap)", textModifier)
169                 }
170             }
171 
172             item {
173                 Column {
174                     // An image who's provider uses the Icon api.
175                     IconImage(contentScale = type, modifier = itemModifier)
176                     Text("ImageProvider(icon)", textModifier)
177                 }
178             }
179         }
180     }
181 }
182 
183 @Composable
ResourceImagenull184 private fun ResourceImage(contentScale: ContentScale, modifier: GlanceModifier = GlanceModifier) {
185     Image(
186         provider = ImageProvider(R.drawable.compose),
187         contentDescription = "Content Scale image sample (value: ${contentScale.asString()})",
188         contentScale = contentScale,
189         modifier = modifier
190     )
191 }
192 
193 /**
194  * Demonstrates using the Uri image provider in `androidx.glance.appwidget`. This image will be sent
195  * to the RemoteViews as a uri. In the AppWidgetHost, the uri will be resolved by querying back to
196  * this app's [ContentProvider], see [ImageAppWidgetImageContentProvider]. There are several
197  * drawbacks to this approach. Consider them before going this route.
198  * - Images that are within the app's private directories can only be exposed via ContentProvider; a
199  *   direct reference via file:// uri will not work.
200  * - The ContentProvider approach will not work across user/work profiles.
201  * - Any time the image is loaded, the AppWidget's process will be started, consuming battery and
202  *   memory.
203  * - FileProvider cannot be used due to a permissions issue.
204  */
205 @Composable
UriImagenull206 private fun UriImage(
207     contentScale: ContentScale,
208     modifier: GlanceModifier = GlanceModifier,
209     uri: Uri
210 ) {
211     Image(
212         provider = ImageProvider(uri),
213         contentDescription = "Content Scale image sample (value: ${contentScale.asString()})",
214         contentScale = contentScale,
215         modifier = modifier
216     )
217 }
218 
219 /**
220  * Bitmaps are passed in memory from the appwidget provider's process to the appwidget host's
221  * process. Be careful to not use too many or too large bitmaps. See [android.widget.RemoteViews]
222  * for more info.
223  */
224 @Composable
BitmapImagenull225 private fun BitmapImage(contentScale: ContentScale, modifier: GlanceModifier = GlanceModifier) {
226     fun makeBitmap(): Bitmap {
227         val w = 100f
228         val h = 100f
229         val bmp = Bitmap.createBitmap(w.toInt(), h.toInt(), Bitmap.Config.ARGB_8888)
230         val canvas = Canvas(bmp)
231         val paint = Paint()
232 
233         paint.setColor(Color.Black.toArgb()) // transparent
234         canvas.drawRect(0f, 0f, w, h, paint)
235         paint.setColor(Color.White.toArgb()) // Opaque
236         canvas.drawCircle(w / 2f, h / 2f, w / 3f, paint)
237 
238         return bmp
239     }
240 
241     Image(
242         provider = ImageProvider(makeBitmap()),
243         contentDescription = "An image with an in-memory bitmap provider",
244         contentScale = contentScale,
245         modifier = modifier
246     )
247 }
248 
249 /**
250  * For displaying [Image]s backed by [android.graphics.drawable.Icon]s. Despite the name, an [Icon]
251  * does not need to represent a literal icon.
252  */
253 @Composable
IconImagenull254 private fun IconImage(contentScale: ContentScale, modifier: GlanceModifier) {
255     if (Build.VERSION.SDK_INT < 23) {
256         Text("The Icon api requires api >= 23")
257         return
258     }
259 
260     val bitmap = canvasBitmap(200, circleColor = Color.Red)
261 
262     Box {
263         Image(
264             provider = ImageProvider(bitmap),
265             contentDescription = "An image with an in-memory bitmap provider",
266             contentScale = contentScale,
267             modifier = modifier
268         )
269     }
270 }
271 
canvasBitmapnull272 private fun canvasBitmap(outputCanvasSize: Int, circleColor: Color): Bitmap {
273     val bitmap = Bitmap.createBitmap(outputCanvasSize, outputCanvasSize, Bitmap.Config.ARGB_8888)
274     val padding = outputCanvasSize * .05f
275     val canvas = Canvas(bitmap)
276 
277     fun drawBlueSquare(canvas: Canvas) {
278         val squareSize = outputCanvasSize * (2f / 3f)
279 
280         val x0 = padding
281         val x1 = x0 + squareSize
282         val y0 = (outputCanvasSize - squareSize - padding)
283         val y1 = y0 + squareSize
284         val paint = Paint().apply { setColor(Color.Blue.toArgb()) }
285         canvas.drawRect(x0, y0, x1, y1, paint)
286     }
287 
288     fun drawCircle(canvas: Canvas) {
289         val r = outputCanvasSize * (1f / 3f)
290         val cx = outputCanvasSize - r - padding
291         val cy = r + padding
292 
293         val paint = Paint().apply { setColor(circleColor.toArgb()) }
294         canvas.drawCircle(cx, cy, r, paint)
295     }
296 
297     drawBlueSquare(canvas)
298     drawCircle(canvas)
299 
300     return bitmap
301 }
302 
asStringnull303 private fun ContentScale.asString(): String =
304     when (this) {
305         ContentScale.Fit -> "Fit"
306         ContentScale.FillBounds -> "Fill Bounds"
307         ContentScale.Crop -> "Crop"
308         else -> "Unknown content scale"
309     }
310 
311 private object ShareableImageUtils {
312     private val fileProviderDirectory = "imageAppWidget"
313     private val fileName = "imageToBeLoadedFromUri.png"
314 
urinull315     private fun uri(context: Context, filename: String): Uri {
316         val packageName = context.packageName
317         return Uri.parse("content://$packageName/$filename")
318     }
319 
320     val Context.uriImageFile: File
321         get() = File(this.filesDir, "$fileProviderDirectory/$fileName")
322 
323     /** Create a Uri to share and ensure the file we want to return exists. */
getShareableImageUrinull324     fun getShareableImageUri(context: Context): Uri {
325 
326         val file: File = context.uriImageFile
327         file.parentFile?.mkdir()
328         val success =
329             if (file.exists()) {
330                 true
331             } else {
332                 val bitmap = canvasBitmap(300, circleColor = Color.Green)
333                 file.outputStream().use { out ->
334                     bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
335                 }
336             }
337 
338         if (success) {
339             return uri(context, file.name)
340         } else {
341             throw IllegalStateException("Failed to write bitmap")
342         }
343     }
344 }
345 
346 /** Expose an image file via content:// uri. */
347 class ImageAppWidgetImageContentProvider : ContentProvider() {
onCreatenull348     override fun onCreate(): Boolean {
349         return true
350     }
351 
352     /**
353      * A simplified version of [openFile] for example only. This version does not validate the uri
354      * and always returns the same file.
355      */
openFilenull356     override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
357         val context = context ?: return null
358         val file = context.uriImageFile
359         return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
360     }
361 
querynull362     override fun query(
363         uri: Uri,
364         projection: Array<out String>?,
365         selection: String?,
366         selectionArgs: Array<out String>?,
367         sortOrder: String?
368     ): Cursor? {
369         return null // unused
370     }
371 
getTypenull372     override fun getType(uri: Uri): String? {
373         return "image/png"
374     }
375 
insertnull376     override fun insert(uri: Uri, values: ContentValues?): Uri? {
377         return null // unused
378     }
379 
deletenull380     override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
381         return 0 // unused
382     }
383 
updatenull384     override fun update(
385         uri: Uri,
386         values: ContentValues?,
387         selection: String?,
388         selectionArgs: Array<out String>?
389     ): Int {
390         return 0 // unused
391     }
392 }
393