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