diff --git a/Jetcaster/README.md b/Jetcaster/README.md index 43b85084da..b4cc85cad1 100644 --- a/Jetcaster/README.md +++ b/Jetcaster/README.md @@ -134,6 +134,7 @@ limitations under the License. [epstore]: mobile/src/main/java/com/example/jetcaster/data/EpisodeStore.kt [catstore]: mobile/src/main/java/com/example/jetcaster/data/CategoryStore.kt [db]: mobile/src/main/java/com/example/jetcaster/data/room/JetcasterDatabase.kt + [glance]: https://developer.android.com/develop/ui/compose/glance [homevm]: mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt [homeui]: mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt [compose]: https://developer.android.com/jetpack/compose diff --git a/Jetcaster/glancewidget/build.gradle.kts b/Jetcaster/glancewidget/build.gradle.kts index bbcdbc36c5..a29291be40 100644 --- a/Jetcaster/glancewidget/build.gradle.kts +++ b/Jetcaster/glancewidget/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(libs.coil.kt.compose) implementation(libs.androidx.core.ktx) + implementation(libs.android.material3) implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) diff --git a/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt index 0fa21520e8..c932bbf1e2 100644 --- a/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt +++ b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt @@ -22,8 +22,6 @@ import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.net.Uri import android.util.Log -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -32,6 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -59,7 +58,6 @@ import androidx.glance.layout.Spacer import androidx.glance.layout.fillMaxSize import androidx.glance.layout.padding import androidx.glance.layout.size -import androidx.glance.material3.ColorProviders import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextStyle @@ -70,7 +68,7 @@ import coil.request.ImageRequest import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -internal val TAG = "JetcasterAppWidegt" +internal val TAG = "JetcasterAppWidget" /** * Implementation of App Widget functionality. @@ -85,28 +83,32 @@ data class JetcasterAppWidgetViewState( val podcastTitle: String, val isPlaying: Boolean, val albumArtUri: String, - val useDynamicColor: Boolean ) private object Sizes { + val short = 72.dp val minWidth = 140.dp val smallBucketCutoffWidth = 250.dp // anything from minWidth to this will have no title - val imageNormal = 80.dp - val imageCondensed = 60.dp + val normal = 80.dp + val medium = 56.dp + val condensed = 48.dp } -private enum class SizeBucket { Invalid, Narrow, Normal } +private enum class SizeBucket { Invalid, Narrow, Normal, NarrowShort, NormalShort } @Composable private fun calculateSizeBucket(): SizeBucket { val size: DpSize = LocalSize.current val width = size.width + val height = size.height return when { width < Sizes.minWidth -> SizeBucket.Invalid - width <= Sizes.smallBucketCutoffWidth -> SizeBucket.Narrow - else -> SizeBucket.Normal + width <= Sizes.smallBucketCutoffWidth -> + if (height >= Sizes.short) SizeBucket.Narrow else SizeBucket.NarrowShort + else -> + if (height >= Sizes.short) SizeBucket.Normal else SizeBucket.NormalShort } } @@ -122,8 +124,7 @@ class JetcasterAppWidget : GlanceAppWidget() { podcastTitle = "Now in Android", isPlaying = false, albumArtUri = "https://static.libsyn.com/p/assets/9/f/f/3/" + - "9ff3cb5dc6cfb3e2e5bbc093207a2619/NIA000_PodcastThumbnail.png", - useDynamicColor = false + "9ff3cb5dc6cfb3e2e5bbc093207a2619/NIA000_PodcastThumbnail.png" ) provideContent { @@ -131,20 +132,31 @@ class JetcasterAppWidget : GlanceAppWidget() { val playPauseIcon = if (testState.isPlaying) PlayPauseIcon.Pause else PlayPauseIcon.Play val artUri = Uri.parse(testState.albumArtUri) - GlanceTheme( - colors = ColorProviders( - light = lightColorScheme(), - dark = darkColorScheme() - ) - ) { + GlanceTheme { when (sizeBucket) { SizeBucket.Invalid -> WidgetUiInvalidSize() - SizeBucket.Narrow -> WidgetUiNarrow( + SizeBucket.Narrow -> Widget( + iconSize = Sizes.medium, imageUri = artUri, playPauseIcon = playPauseIcon ) SizeBucket.Normal -> WidgetUiNormal( + iconSize = Sizes.normal, + title = testState.episodeTitle, + subtitle = testState.podcastTitle, + imageUri = artUri, + playPauseIcon = playPauseIcon + ) + + SizeBucket.NarrowShort -> Widget( + iconSize = Sizes.condensed, + imageUri = artUri, + playPauseIcon = playPauseIcon + ) + + SizeBucket.NormalShort -> WidgetUiNormal( + iconSize = Sizes.condensed, title = testState.episodeTitle, subtitle = testState.podcastTitle, imageUri = artUri, @@ -162,20 +174,23 @@ private fun WidgetUiNormal( subtitle: String, imageUri: Uri, playPauseIcon: PlayPauseIcon, + iconSize: Dp, ) { - Scaffold(titleBar = {} /* title bar will be optional starting in glance 1.1.0-beta3*/) { + + Scaffold { Row( GlanceModifier.fillMaxSize(), verticalAlignment = Alignment.Vertical.CenterVertically ) { - AlbumArt(imageUri, GlanceModifier.size(Sizes.imageNormal)) + AlbumArt(imageUri, GlanceModifier.size(iconSize)) PodcastText(title, subtitle, modifier = GlanceModifier.padding(16.dp).defaultWeight()) - PlayPauseButton(playPauseIcon, {}) + PlayPauseButton(GlanceModifier.size(iconSize), playPauseIcon, {}) } } } @Composable -private fun WidgetUiNarrow( +private fun Widget( + iconSize: Dp, imageUri: Uri, playPauseIcon: PlayPauseIcon, ) { @@ -184,9 +199,9 @@ private fun WidgetUiNarrow( modifier = GlanceModifier.fillMaxSize(), verticalAlignment = Alignment.Vertical.CenterVertically ) { - AlbumArt(imageUri, GlanceModifier.size(Sizes.imageCondensed)) + AlbumArt(imageUri, GlanceModifier.size(iconSize)) Spacer(GlanceModifier.defaultWeight()) - PlayPauseButton(playPauseIcon, {}) + PlayPauseButton(GlanceModifier.size(iconSize), playPauseIcon, {}) } } } @@ -209,22 +224,44 @@ private fun AlbumArt( @Composable fun PodcastText(title: String, subtitle: String, modifier: GlanceModifier = GlanceModifier) { val fgColor = GlanceTheme.colors.onPrimaryContainer - Column(modifier) { - Text( - text = title, - style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Medium, color = fgColor), - maxLines = 2, - ) - Text( - text = subtitle, - style = TextStyle(fontSize = 14.sp, color = fgColor), - maxLines = 2, - ) + val size = LocalSize.current + when { + size.height >= Sizes.short -> Column(modifier) { + Text( + text = title, + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = fgColor + ), + maxLines = 2, + ) + Text( + text = subtitle, + style = TextStyle(fontSize = 14.sp, color = fgColor), + maxLines = 2, + ) + } + else -> Column(modifier) { + Text( + text = title, + style = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = fgColor + ), + maxLines = 1, + ) + } } } @Composable -private fun PlayPauseButton(state: PlayPauseIcon, onClick: () -> Unit) { +private fun PlayPauseButton( + modifier: GlanceModifier = GlanceModifier.size(Sizes.normal), + state: PlayPauseIcon, + onClick: () -> Unit +) { val (iconRes: Int, description: Int) = when (state) { PlayPauseIcon.Play -> R.drawable.outline_play_arrow_24 to R.string.content_description_play PlayPauseIcon.Pause -> R.drawable.outline_pause_24 to R.string.content_description_pause @@ -234,7 +271,8 @@ private fun PlayPauseButton(state: PlayPauseIcon, onClick: () -> Unit) { val contentDescription = LocalContext.current.getString(description) SquareIconButton( - provider, + modifier = modifier, + imageProvider = provider, contentDescription = contentDescription, onClick = onClick ) diff --git a/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidgetPreview.kt b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidgetPreview.kt new file mode 100644 index 0000000000..8e26736f5b --- /dev/null +++ b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidgetPreview.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2024 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 + * + * https://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 com.example.jetcaster.glancewidget + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.content.Context +import android.os.Build +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.SizeMode +import androidx.glance.appwidget.components.Scaffold +import androidx.glance.appwidget.components.SquareIconButton +import androidx.glance.appwidget.compose +import androidx.glance.appwidget.provideContent +import androidx.glance.layout.Alignment +import androidx.glance.layout.Row +import androidx.glance.layout.Spacer +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.size +import androidx.glance.layout.wrapContentSize +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +private object SizesPreview { + val medium = 56.dp +} + +/** + * This is a convenience function for updating the widget preview using Generated Previews. + * + * In a real application, this would be called whenever the widget's state changes. + */ +fun updateWidgetPreview(context: Context) { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + CoroutineScope(Dispatchers.IO).launch { + try { + val appwidgetManager = AppWidgetManager.getInstance(context) + + appwidgetManager.setWidgetPreview( + ComponentName(context, JetcasterAppWidgetReceiver::class.java), + AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN, + JetcasterAppWidgetPreview().compose( + context, + size = DpSize(160.dp, 64.dp) + ), + ) + } catch (e: Exception) { + Log.e(TAG, e.message, e) + } + } + } +} + +class JetcasterAppWidgetPreview : GlanceAppWidget() { + override val sizeMode: SizeMode + get() = SizeMode.Exact + + override suspend fun provideGlance(context: Context, id: GlanceId) { + + provideContent { + GlanceTheme { + Widget() + } + } + } +} + +@Composable +private fun Widget() { + + Scaffold { + Row( + modifier = GlanceModifier.fillMaxSize(), + verticalAlignment = Alignment.Vertical.CenterVertically + ) { + Image( + modifier = GlanceModifier.wrapContentSize().size(SizesPreview.medium), + provider = ImageProvider(R.drawable.widget_preview_thumbnail), + contentDescription = "" + ) + Spacer(GlanceModifier.defaultWeight()) + SquareIconButton( + modifier = GlanceModifier.size(SizesPreview.medium), + imageProvider = ImageProvider(R.drawable.outline_play_arrow_24), + contentDescription = "", + onClick = { } + ) + } + } +} diff --git a/Jetcaster/glancewidget/src/main/res/drawable/outline_play_arrow_24.xml b/Jetcaster/glancewidget/src/main/res/drawable/outline_play_arrow_24.xml index 91ab3ac149..3588d6f062 100644 --- a/Jetcaster/glancewidget/src/main/res/drawable/outline_play_arrow_24.xml +++ b/Jetcaster/glancewidget/src/main/res/drawable/outline_play_arrow_24.xml @@ -1,4 +1,4 @@ - + diff --git a/Jetcaster/glancewidget/src/main/res/drawable/widget_preview.png b/Jetcaster/glancewidget/src/main/res/drawable/widget_preview.png new file mode 100644 index 0000000000..eacc0ff4d7 Binary files /dev/null and b/Jetcaster/glancewidget/src/main/res/drawable/widget_preview.png differ diff --git a/Jetcaster/glancewidget/src/main/res/drawable/widget_preview_image_shape.xml b/Jetcaster/glancewidget/src/main/res/drawable/widget_preview_image_shape.xml new file mode 100644 index 0000000000..24dcb456d9 --- /dev/null +++ b/Jetcaster/glancewidget/src/main/res/drawable/widget_preview_image_shape.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/Jetcaster/glancewidget/src/main/res/drawable/widget_preview_thumbnail.png b/Jetcaster/glancewidget/src/main/res/drawable/widget_preview_thumbnail.png new file mode 100644 index 0000000000..0a33505bc8 Binary files /dev/null and b/Jetcaster/glancewidget/src/main/res/drawable/widget_preview_thumbnail.png differ diff --git a/Jetcaster/glancewidget/src/main/res/layout/widget_preview.xml b/Jetcaster/glancewidget/src/main/res/layout/widget_preview.xml new file mode 100644 index 0000000000..8053af954f --- /dev/null +++ b/Jetcaster/glancewidget/src/main/res/layout/widget_preview.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Jetcaster/glancewidget/src/main/res/values-h48dp/sizes.xml b/Jetcaster/glancewidget/src/main/res/values-h48dp/sizes.xml new file mode 100644 index 0000000000..f9321a0331 --- /dev/null +++ b/Jetcaster/glancewidget/src/main/res/values-h48dp/sizes.xml @@ -0,0 +1,4 @@ + + + 58dp + \ No newline at end of file diff --git a/Jetcaster/glancewidget/src/main/res/values-night-v31/colors.xml b/Jetcaster/glancewidget/src/main/res/values-night-v31/colors.xml new file mode 100644 index 0000000000..8348815e90 --- /dev/null +++ b/Jetcaster/glancewidget/src/main/res/values-night-v31/colors.xml @@ -0,0 +1,19 @@ + + + + @android:color/system_accent2_800 + diff --git a/Jetcaster/glancewidget/src/main/res/values-night/colors.xml b/Jetcaster/glancewidget/src/main/res/values-night/colors.xml new file mode 100644 index 0000000000..3e00c7293e --- /dev/null +++ b/Jetcaster/glancewidget/src/main/res/values-night/colors.xml @@ -0,0 +1,19 @@ + + + + #ff20333d + diff --git a/Jetcaster/glancewidget/src/main/res/values-v31/colors.xml b/Jetcaster/glancewidget/src/main/res/values-v31/colors.xml new file mode 100644 index 0000000000..b9536a2ec5 --- /dev/null +++ b/Jetcaster/glancewidget/src/main/res/values-v31/colors.xml @@ -0,0 +1,19 @@ + + + + @android:color/system_accent2_50 + diff --git a/Jetcaster/glancewidget/src/main/res/values-v31/styles.xml b/Jetcaster/glancewidget/src/main/res/values-v31/styles.xml new file mode 100644 index 0000000000..7f5c58270b --- /dev/null +++ b/Jetcaster/glancewidget/src/main/res/values-v31/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/Jetcaster/glancewidget/src/main/res/values/colors.xml b/Jetcaster/glancewidget/src/main/res/values/colors.xml new file mode 100644 index 0000000000..b545d6af07 --- /dev/null +++ b/Jetcaster/glancewidget/src/main/res/values/colors.xml @@ -0,0 +1,23 @@ + + + + + #FFECDCFF + #FF7CD7BA + #FF2C322F + #ffe0f3ff + diff --git a/Jetcaster/glancewidget/src/main/res/values/sizes.xml b/Jetcaster/glancewidget/src/main/res/values/sizes.xml new file mode 100644 index 0000000000..0cda911ac9 --- /dev/null +++ b/Jetcaster/glancewidget/src/main/res/values/sizes.xml @@ -0,0 +1,4 @@ + + + 80dp + \ No newline at end of file diff --git a/Jetcaster/glancewidget/src/main/res/values/styles.xml b/Jetcaster/glancewidget/src/main/res/values/styles.xml new file mode 100644 index 0000000000..eb4c694eed --- /dev/null +++ b/Jetcaster/glancewidget/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/Jetcaster/glancewidget/src/main/res/xml/jetcaster_info.xml b/Jetcaster/glancewidget/src/main/res/xml/jetcaster_info.xml index d38f1d1b5e..6391582b2d 100644 --- a/Jetcaster/glancewidget/src/main/res/xml/jetcaster_info.xml +++ b/Jetcaster/glancewidget/src/main/res/xml/jetcaster_info.xml @@ -1,10 +1,16 @@