diff --git a/Jetchat/app/build.gradle.kts b/Jetchat/app/build.gradle.kts index 4d325b2b74..ae39cd288d 100644 --- a/Jetchat/app/build.gradle.kts +++ b/Jetchat/app/build.gradle.kts @@ -24,12 +24,13 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = 2 namespace = "com.example.compose.jetchat" defaultConfig { applicationId = "com.example.compose.jetchat" minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() + targetSdk = 37 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BackdropBlurModifier.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BackdropBlurModifier.kt new file mode 100644 index 0000000000..c6560d6028 --- /dev/null +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BackdropBlurModifier.kt @@ -0,0 +1,363 @@ +/* + * Copyright 2026 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.compose.jetchat.blur + +import android.graphics.RenderEffect +import android.graphics.RenderNode +import android.graphics.Shader +import android.os.Build +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Specification for blur radius parameters, capable of creating a hardware [RenderEffect] + * for use with [RenderNode.setBackdropRenderEffect]. + * + * @param radiusX The horizontal blur radius. + * @param radiusY The vertical blur radius (defaults to [radiusX]). + * @param tileMode The tile mode for handling edges (defaults to [Shader.TileMode.CLAMP]). + */ +data class BlurRadiusSpec(val radiusX: Dp, val radiusY: Dp = radiusX, val tileMode: Shader.TileMode = Shader.TileMode.CLAMP) { + /** + * Creates an Android [RenderEffect] configured with this specification. + */ + fun createRenderEffect(density: Density): RenderEffect? { + val rxPx = with(density) { radiusX.toPx() } + val ryPx = with(density) { radiusY.toPx() } + return createRenderEffect(rxPx, ryPx, tileMode) + } + + companion object { + /** + * Creates a hardware [RenderEffect] blur effect from pixel radii. + */ + fun createRenderEffect( + radiusXPx: Float, + radiusYPx: Float = radiusXPx, + tileMode: Shader.TileMode = Shader.TileMode.CLAMP, + ): RenderEffect? { + if (Build.VERSION.SDK_INT >= 31 && (radiusXPx > 0f || radiusYPx > 0f)) { + return RenderEffect.createBlurEffect( + radiusXPx.coerceAtLeast(0.01f), + radiusYPx.coerceAtLeast(0.01f), + tileMode, + ) + } + return null + } + + /** + * Creates a hardware [RenderEffect] blur effect from a [Dp] radius. + */ + fun createRenderEffect(radius: Dp, density: Density, tileMode: Shader.TileMode = Shader.TileMode.CLAMP): RenderEffect? { + val px = with(density) { radius.toPx() } + return createRenderEffect(px, px, tileMode) + } + } +} + +/** + * Applies an in-window backdrop [RenderEffect] to content drawn behind this composable in the window. + * + * On supported platforms (Android 17 / SDK 37+), this leverages [RenderNode.setBackdropRenderEffect] + * to apply hardware-accelerated visual effects (like blur) to the backdrop before this composable + * is drawn, enabling translucent floating navigation bars, top app bars, and frosted-glass cards. + * + * @param renderEffect The [RenderEffect] to apply to the backdrop behind this composable. + * @param shape The shape used to clip the backdrop effect and outline. + * @param tint An optional translucent color overlay drawn on top of the backdrop effect. + * @param elevation Optional elevation shadow cast by this component. + * @param outerShadowOnly If true, clips out the shadow cast beneath the outline area so the shadow + * does not darken the translucent frosted glass interior. + * @param fallbackColor An optional fallback background color for platforms earlier than Android 17. + */ +fun Modifier.backdropRenderEffect( + renderEffect: RenderEffect?, + shape: Shape = RectangleShape, + tint: Color = Color.Unspecified, + elevation: Dp = 0.dp, + outerShadowOnly: Boolean = true, + fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent, +): Modifier = this then BackdropRenderEffectElement( + renderEffect = renderEffect, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, +) + +/** + * Draws the content behind this composable blurred with the specified [radius], + * clipped to [shape], beneath this composable's own content. + * + * @param radius The blur radius to apply to the backdrop. + * @param shape The shape of the frosted-glass region. + * @param tint An optional translucent color overlay drawn over the blurred backdrop. + * @param elevation Optional elevation shadow cast by this component. + * @param outerShadowOnly If true, clips out the shadow cast beneath the outline area. + * @param fallbackColor An optional fallback background color for platforms earlier than Android 17. + */ +fun Modifier.backdropBlur( + radius: Dp, + shape: Shape = RectangleShape, + tint: Color = Color.Unspecified, + elevation: Dp = 0.dp, + outerShadowOnly: Boolean = true, + fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent, +): Modifier = backdropBlur( + radiusX = radius, + radiusY = radius, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, +) + +/** + * Overload of [backdropBlur] allowing independent horizontal and vertical blur radii. + * + * @param radiusX The horizontal blur radius. + * @param radiusY The vertical blur radius. + * @param shape The shape of the frosted-glass region. + * @param tint An optional translucent color overlay drawn over the blurred backdrop. + * @param elevation Optional elevation shadow cast by this component. + * @param outerShadowOnly If true, clips out the shadow cast beneath the outline area. + * @param fallbackColor An optional fallback background color for platforms earlier than Android 17. + */ +fun Modifier.backdropBlur( + radiusX: Dp, + radiusY: Dp, + shape: Shape = RectangleShape, + tint: Color = Color.Unspecified, + elevation: Dp = 0.dp, + outerShadowOnly: Boolean = true, + fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent, +): Modifier = backdropBlur( + spec = BlurRadiusSpec(radiusX, radiusY), + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, +) + +/** + * Overload of [backdropBlur] configured via a [BlurRadiusSpec]. + */ +fun Modifier.backdropBlur( + spec: BlurRadiusSpec, + shape: Shape = RectangleShape, + tint: Color = Color.Unspecified, + elevation: Dp = 0.dp, + outerShadowOnly: Boolean = true, + fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent, +): Modifier = this then BackdropBlurElement( + spec = spec, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, +) + +private data class BackdropRenderEffectElement( + val renderEffect: RenderEffect?, + val shape: Shape, + val tint: Color, + val elevation: Dp, + val outerShadowOnly: Boolean, + val fallbackColor: Color, +) : ModifierNodeElement() { + override fun create(): BackdropRenderEffectNode = BackdropRenderEffectNode( + renderEffect = renderEffect, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) + + override fun update(node: BackdropRenderEffectNode) { + node.update( + renderEffect = renderEffect, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) + } + + override fun InspectorInfo.inspectableProperties() { + name = "backdropRenderEffect" + properties["renderEffect"] = renderEffect + properties["shape"] = shape + properties["tint"] = tint + properties["elevation"] = elevation + properties["outerShadowOnly"] = outerShadowOnly + properties["fallbackColor"] = fallbackColor + } +} + +private data class BackdropBlurElement( + val spec: BlurRadiusSpec, + val shape: Shape, + val tint: Color, + val elevation: Dp, + val outerShadowOnly: Boolean, + val fallbackColor: Color, +) : ModifierNodeElement() { + override fun create(): BackdropBlurNode = BackdropBlurNode( + spec = spec, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) + + override fun update(node: BackdropBlurNode) { + node.update( + spec = spec, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) + } + + override fun InspectorInfo.inspectableProperties() { + name = "backdropBlur" + properties["spec"] = spec + properties["shape"] = shape + properties["tint"] = tint + properties["elevation"] = elevation + properties["outerShadowOnly"] = outerShadowOnly + properties["fallbackColor"] = fallbackColor + } +} + +private class BackdropRenderEffectNode( + var renderEffect: RenderEffect?, + shape: Shape, + tint: Color, + elevation: Dp, + outerShadowOnly: Boolean, + fallbackColor: Color, +) : BaseBackdropNode(shape, tint, elevation, outerShadowOnly, fallbackColor) { + + override fun resolveRenderEffect(density: Density): RenderEffect? = renderEffect + + fun update(renderEffect: RenderEffect?, shape: Shape, tint: Color, elevation: Dp, outerShadowOnly: Boolean, fallbackColor: Color) { + var changed = false + + if (this.renderEffect != renderEffect) { + this.renderEffect = renderEffect + changed = true + } + if (this.shape != shape) { + this.shape = shape + changed = true + } + if (this.tint != tint) { + this.tint = tint + changed = true + } + if (this.elevation != elevation) { + this.elevation = elevation + changed = true + } + if (this.outerShadowOnly != outerShadowOnly) { + this.outerShadowOnly = outerShadowOnly + changed = true + } + if (this.fallbackColor != fallbackColor) { + this.fallbackColor = fallbackColor + changed = true + } + if (changed) { + markDirty() + } + } +} + +private class BackdropBlurNode( + var spec: BlurRadiusSpec, + shape: Shape, + tint: Color, + elevation: Dp, + outerShadowOnly: Boolean, + fallbackColor: Color, +) : BaseBackdropNode(shape, tint, elevation, outerShadowOnly, fallbackColor) { + + private var cachedEffect: RenderEffect? = null + private var cachedDensity: Float = -1f + private var cachedSpec: BlurRadiusSpec? = null + + override fun resolveRenderEffect(density: Density): RenderEffect? { + val currentDensity = density.density + if (cachedEffect == null || cachedDensity != currentDensity || cachedSpec != spec) { + cachedEffect = spec.createRenderEffect(density) + cachedDensity = currentDensity + cachedSpec = spec + } + return cachedEffect + } + + fun update(spec: BlurRadiusSpec, shape: Shape, tint: Color, elevation: Dp, outerShadowOnly: Boolean, fallbackColor: Color) { + var changed = false + if (this.spec != spec) { + this.spec = spec + cachedEffect = null + changed = true + } + if (this.shape != shape) { + this.shape = shape + changed = true + } + if (this.tint != tint) { + this.tint = tint + changed = true + } + if (this.elevation != elevation) { + this.elevation = elevation + changed = true + } + if (this.outerShadowOnly != outerShadowOnly) { + this.outerShadowOnly = outerShadowOnly + changed = true + } + if (this.fallbackColor != fallbackColor) { + this.fallbackColor = fallbackColor + changed = true + } + if (changed) { + markDirty() + } + } +} diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BaseBackdropNode.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BaseBackdropNode.kt new file mode 100644 index 0000000000..48fa0b298e --- /dev/null +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BaseBackdropNode.kt @@ -0,0 +1,243 @@ +/* + * Copyright 2026 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.compose.jetchat.blur + +import android.annotation.SuppressLint +import android.graphics.Outline as AndroidOutline +import android.graphics.Paint +import android.graphics.Path as AndroidPath +import android.graphics.RenderEffect +import android.graphics.RenderNode +import android.os.Build +import android.util.Log +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.asAndroidPath +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import kotlin.math.roundToInt + +abstract class BaseBackdropNode( + var shape: Shape, + var tint: Color, + var elevation: Dp, + var outerShadowOnly: Boolean, + var fallbackColor: Color, +) : Modifier.Node(), + DrawModifierNode { + + abstract fun resolveRenderEffect(density: Density): RenderEffect? + + private var renderNode: RenderNode? = null + private val androidOutline = AndroidOutline() + private val tintPaint = Paint() + + private var lastWidth = -1 + private var lastHeight = -1 + private var lastRenderEffect: RenderEffect? = null + private var lastDensity = -1f + private var lastShape: Shape? = null + private var lastLayoutDirection: LayoutDirection? = null + private var lastElevationPx = -1f + private var lastTint: Color = Color.Unspecified + private var isDirty = true + + protected fun markDirty() { + isDirty = true + invalidateDraw() + } + + override fun onDetach() { + if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN) { + renderNode?.discardDisplayList() + } + lastWidth = -1 + lastHeight = -1 + isDirty = true + } + + override fun ContentDrawScope.draw() { + val effect = resolveRenderEffect(this) + if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN && effect != null) { + val widthPx = size.width.roundToInt() + val heightPx = size.height.roundToInt() + + if (widthPx <= 0 || heightPx <= 0) { + drawContent() + return + } + + var node = renderNode + if (node == null) { + node = RenderNode("BackdropRenderEffectNode").apply { + clipToOutline = true + } + renderNode = node + isDirty = true + } + + val elevationPx = elevation.toPx() + val densityVal = density + + if (isDirty || + widthPx != lastWidth || + heightPx != lastHeight || + effect != lastRenderEffect || + densityVal != lastDensity || + shape != lastShape || + layoutDirection != lastLayoutDirection || + elevationPx != lastElevationPx || + tint != lastTint + ) { + lastWidth = widthPx + lastHeight = heightPx + lastRenderEffect = effect + lastDensity = densityVal + lastShape = shape + lastLayoutDirection = layoutDirection + lastElevationPx = elevationPx + lastTint = tint + isDirty = false + + // Configure RenderNode geometry & effect + node.setPosition(0, 0, widthPx, heightPx) + try { + node.setBackdropRenderEffect(effect) + } catch (t: Throwable) { + Log.w("BackdropBlur", "Failed to setBackdropRenderEffect: ${t.message}") + } + + // Map Compose Shape to Android Outline + val composeOutline = shape.createOutline(size, layoutDirection, this) + updateAndroidOutline(androidOutline, composeOutline, widthPx, heightPx) + + if (elevationPx > 0f) { + node.elevation = elevationPx + try { + androidOutline.isOuterShadowOnly = outerShadowOnly + } catch (_: Throwable) { + // Ignore if setOuterShadowOnly is not available + } + } else { + node.elevation = 0f + } + + node.setOutline(androidOutline) + node.clipToOutline = true + + // Record backdrop tint/wash inside RenderNode + val recordingCanvas = node.beginRecording(widthPx, heightPx) + if (tint.isSpecified && tint.alpha > 0f) { + tintPaint.color = tint.toArgb() + recordingCanvas.drawRect(0f, 0f, widthPx.toFloat(), heightPx.toFloat(), tintPaint) + } + node.endRecording() + } + + // 1. Draw hardware backdrop RenderNode + drawIntoCanvas { canvas -> + canvas.nativeCanvas.drawRenderNode(node) + } + + // 2. Draw composable content on top + drawContent() + } else { + // Fallback for pre-API 37 or null effect + if (fallbackColor.isSpecified && fallbackColor.alpha > 0f) { + drawOutline( + outline = shape.createOutline(size, layoutDirection, this), + color = fallbackColor, + ) + } + drawContent() + } + } +} + +/** + * Helper to populate an [AndroidOutline] from a Compose [Outline]. + */ +private fun updateAndroidOutline(androidOutline: AndroidOutline, composeOutline: Outline, width: Int, height: Int) { + androidOutline.alpha = 1.0f + when (composeOutline) { + is Outline.Rectangle -> { + androidOutline.setRect(0, 0, width, height) + } + + is Outline.Rounded -> { + val rect = composeOutline.roundRect + val radius = rect.topLeftCornerRadius.x + val topLeft = rect.topLeftCornerRadius + val topRight = rect.topRightCornerRadius + val bottomLeft = rect.bottomLeftCornerRadius + val bottomRight = rect.bottomRightCornerRadius + + if (topLeft == topRight && topLeft == bottomLeft && topLeft == bottomRight && topLeft.x == topLeft.y) { + // Uniform corner radii: use setRoundRect with scalar radius + androidOutline.setRoundRect(0, 0, width, height, radius) + } else { + // Complex corner radii: convert to Path + val path = AndroidPath().apply { + addRoundRect( + 0f, + 0f, + width.toFloat(), + height.toFloat(), + floatArrayOf( + rect.topLeftCornerRadius.x, + rect.topLeftCornerRadius.y, + rect.topRightCornerRadius.x, + rect.topRightCornerRadius.y, + rect.bottomRightCornerRadius.x, + rect.bottomRightCornerRadius.y, + rect.bottomLeftCornerRadius.x, + rect.bottomLeftCornerRadius.y, + ), + AndroidPath.Direction.CW, + ) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + androidOutline.setPath(path) + } else { + @Suppress("DEPRECATION") + androidOutline.setConvexPath(path) + } + } + } + + is Outline.Generic -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + androidOutline.setPath(composeOutline.path.asAndroidPath()) + } else { + @Suppress("DEPRECATION") + androidOutline.setConvexPath(composeOutline.path.asAndroidPath()) + } + } + } +} diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/FrostedGlassModifier.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/FrostedGlassModifier.kt new file mode 100644 index 0000000000..17078760bb --- /dev/null +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/FrostedGlassModifier.kt @@ -0,0 +1,278 @@ +/* + * Copyright 2026 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.compose.jetchat.blur + +import android.graphics.RenderEffect +import android.graphics.RuntimeShader +import android.graphics.Shader +import android.os.Build +import android.util.Log +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Specification for a frosted glass backdrop effect chaining hardware blur and fractal noise texture. + * + * @param blurRadius The radius of the blur applied to the backdrop. + * @param noiseFrequency Spatial frequency of the fractal noise (controls grain scale). + * @param noiseIntensity Intensity of the frosted glass noise texture blended over the blurred backdrop. + * @param tileMode Edge handling mode for the blur effect. + */ +data class FrostedGlassSpec( + val blurRadius: Dp = 16.dp, + val noiseFrequency: Float = 0.05f, + val noiseIntensity: Float = 0.05f, + val tileMode: Shader.TileMode = Shader.TileMode.CLAMP, +) { + /** + * Creates a chained hardware [android.graphics.RenderEffect] applying blur and frosted fractal noise texture. + */ + fun createRenderEffect(density: Density): RenderEffect? { + val blurPx = with(density) { blurRadius.toPx() } + return createFrostedGlassEffect( + blurRadiusPx = blurPx, + noiseFrequency = noiseFrequency, + noiseIntensity = noiseIntensity, + tileMode = tileMode, + ) + } + + companion object { + /** + * Creates a chained [RenderEffect] combining blur and frosted fractal noise texture. + */ + fun createRenderEffect( + blurRadius: Dp, + density: Density, + noiseFrequency: Float = 0.05f, + noiseIntensity: Float = 0.05f, + tileMode: Shader.TileMode = Shader.TileMode.CLAMP, + ): RenderEffect? { + val blurPx = with(density) { blurRadius.toPx() } + return createFrostedGlassEffect( + blurRadiusPx = blurPx, + noiseFrequency = noiseFrequency, + noiseIntensity = noiseIntensity, + tileMode = tileMode, + ) + } + } +} + +/** + * Draws the content behind this composable with a frosted glass effect chaining + * hardware blur and fractal noise texture, clipped to [shape], beneath this composable's content. + * + * @param blurRadius The blur radius applied to the backdrop. + * @param noiseFrequency Spatial frequency of the fractal noise (controls grain scale). + * @param noiseIntensity Intensity of the frosted glass noise texture blended over the blurred backdrop. + * @param shape The shape of the frosted-glass region. + * @param tint An optional translucent color overlay drawn over the backdrop. + * @param elevation Optional elevation shadow cast by this component. + * @param outerShadowOnly If true, clips out the shadow cast beneath the outline area. + * @param fallbackColor An optional fallback background color for platforms earlier than Android 17. + */ +fun Modifier.backdropFrostedGlass( + blurRadius: Dp = 16.dp, + noiseFrequency: Float = 0.05f, + noiseIntensity: Float = 0.05f, + shape: Shape = RectangleShape, + tint: Color = Color.Unspecified, + elevation: Dp = 0.dp, + outerShadowOnly: Boolean = true, + fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent, +): Modifier = backdropFrostedGlass( + spec = FrostedGlassSpec( + blurRadius = blurRadius, + noiseFrequency = noiseFrequency, + noiseIntensity = noiseIntensity, + ), + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, +) + +/** + * Overload of [backdropFrostedGlass] configured via a [FrostedGlassSpec]. + */ +fun Modifier.backdropFrostedGlass( + spec: FrostedGlassSpec, + shape: Shape = RectangleShape, + tint: Color = Color.Unspecified, + elevation: Dp = 0.dp, + outerShadowOnly: Boolean = true, + fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent, +): Modifier = this then BackdropFrostedGlassElement( + spec = spec, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, +) + +private data class BackdropFrostedGlassElement( + val spec: FrostedGlassSpec, + val shape: Shape, + val tint: Color, + val elevation: Dp, + val outerShadowOnly: Boolean, + val fallbackColor: Color, +) : ModifierNodeElement() { + override fun create(): BackdropFrostedGlassNode = BackdropFrostedGlassNode( + spec = spec, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) + + override fun update(node: BackdropFrostedGlassNode) { + node.update( + spec = spec, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) + } + + override fun InspectorInfo.inspectableProperties() { + name = "backdropFrostedGlass" + properties["spec"] = spec + properties["shape"] = shape + properties["tint"] = tint + properties["elevation"] = elevation + properties["outerShadowOnly"] = outerShadowOnly + properties["fallbackColor"] = fallbackColor + } +} + +private class BackdropFrostedGlassNode( + var spec: FrostedGlassSpec, + shape: Shape, + tint: Color, + elevation: Dp, + outerShadowOnly: Boolean, + fallbackColor: Color, +) : BaseBackdropNode(shape, tint, elevation, outerShadowOnly, fallbackColor) { + + private var cachedEffect: RenderEffect? = null + private var cachedDensity: Float = -1f + private var cachedSpec: FrostedGlassSpec? = null + + override fun resolveRenderEffect(density: Density): RenderEffect? { + val currentDensity = density.density + if (cachedEffect == null || cachedDensity != currentDensity || cachedSpec != spec) { + cachedEffect = spec.createRenderEffect(density) + cachedDensity = currentDensity + cachedSpec = spec + } + return cachedEffect + } + + fun update(spec: FrostedGlassSpec, shape: Shape, tint: Color, elevation: Dp, outerShadowOnly: Boolean, fallbackColor: Color) { + var changed = false + if (this.spec != spec) { + this.spec = spec + cachedEffect = null + changed = true + } + if (this.shape != shape) { + this.shape = shape + changed = true + } + if (this.tint != tint) { + this.tint = tint + changed = true + } + if (this.elevation != elevation) { + this.elevation = elevation + changed = true + } + if (this.outerShadowOnly != outerShadowOnly) { + this.outerShadowOnly = outerShadowOnly + changed = true + } + if (this.fallbackColor != fallbackColor) { + this.fallbackColor = fallbackColor + changed = true + } + if (changed) { + markDirty() + } + } +} + +/** + * Creates a chained hardware [RenderEffect] combining blur and frosted fractal noise texture. + * + * Chains: + * 1. Inner effect: Hardware blur filter applied first to the backdrop. + * 2. Outer effect: [RuntimeShader] applying frosted fractal noise grain on top of the blurred backdrop. + */ +fun createFrostedGlassEffect( + blurRadiusPx: Float, + noiseFrequency: Float = 0.05f, + noiseIntensity: Float = 0.05f, + tileMode: Shader.TileMode = Shader.TileMode.CLAMP, +): RenderEffect? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return null + + val blurEffect = if (blurRadiusPx > 0f) { + RenderEffect.createBlurEffect( + blurRadiusPx.coerceAtLeast(0.01f), + blurRadiusPx.coerceAtLeast(0.01f), + tileMode, + ) + } else null + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && noiseIntensity > 0f) { + try { + val shader = RuntimeShader(FRACTAL_NOISE_SHADER).apply { + setFloatUniform("frequency", noiseFrequency) + setFloatUniform("noiseIntensity", noiseIntensity) + } + + val noiseEffect = RenderEffect.createRuntimeShaderEffect(shader, "content") + + return if (blurEffect != null) { + // inner = blurEffect (blurs the backdrop first) + // outer = noiseEffect (applies frosted noise grain on top of the blurred backdrop) + RenderEffect.createChainEffect(noiseEffect, blurEffect) + } else { + noiseEffect + } + } catch (t: Throwable) { + Log.w("BackdropBlur", "Failed to create RuntimeShader noise: ${t.message}") + } + } + + return blurEffect +} diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/NoiseShader.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/NoiseShader.kt new file mode 100644 index 0000000000..b4b9e3546d --- /dev/null +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/NoiseShader.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2026 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.compose.jetchat.blur + +/** + * AGSL shader generating procedural fractal noise (equivalent to SVG feTurbulence type="fractalNoise") + * blended over the backdrop content without coordinate displacement. + */ +const val FRACTAL_NOISE_SHADER = """ + uniform shader content; + uniform float frequency; + uniform float noiseIntensity; + + float2 mod289(float2 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; + } + + float3 mod289(float3 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; + } + + float3 permute(float3 x) { + return mod289(((x * 34.0) + 1.0) * x); + } + + // Stefan Gustavson's deterministic 2D Simplex Noise + float simplexNoise2D(float2 v) { + const float4 C = float4( + 0.211324865405187, // (3.0-sqrt(3.0))/6.0 + 0.366025403784439, // 0.5*(sqrt(3.0)-1.0) + -0.577350269189626, // -1.0 + 2.0 * C.x + 0.024390243902439 // 1.0 / 41.0 + ); + + float2 i = floor(v + dot(v, C.yy)); + float2 x0 = v - i + dot(i, C.xx); + + float2 i1 = (x0.x > x0.y) ? float2(1.0, 0.0) : float2(0.0, 1.0); + float4 x12 = x0.xyxy + C.xxzz; + x12.xy -= i1; + + i = mod289(i); + float3 p = permute(permute(i.y + float3(0.0, i1.y, 1.0)) + i.x + float3(0.0, i1.x, 1.0)); + + float3 m = max(0.5 - float3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0); + m = m * m; + m = m * m; + + float3 x = 2.0 * fract(p * C.w) - 1.0; + float3 h = abs(x) - 0.5; + float3 ox = floor(x + 0.5); + float3 a0 = x - ox; + + m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h); + + float3 g; + g.x = a0.x * x0.x + h.x * x0.y; + g.yz = a0.yz * x12.xz + h.yz * x12.yw; + return 130.0 * dot(m, g); + } + + float fractalNoise(float2 p) { + float n0 = simplexNoise2D(p); + float n1 = simplexNoise2D(p * 2.0); + float n2 = simplexNoise2D(p * 4.0); + return (n0 + n1 * 0.5 + n2 * 0.25) / 1.75; + } + + half4 main(float2 fragCoord) { + // Snap to pixel center to eliminate sub-pixel floating-point jitter across redraws + float2 pixelCoord = floor(fragCoord) + 0.5; + float noise = fractalNoise(pixelCoord * frequency); + half4 color = content.eval(fragCoord); + // Add subtle frosted glass surface grain without displacing backdrop coordinates + color.rgb = clamp(color.rgb + noise * (noiseIntensity * color.a), 0.0, color.a); + return color; + } +""" diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt index 86250f1187..f76636b0b3 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt @@ -25,9 +25,11 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -48,6 +50,10 @@ fun JetchatAppBar( actions = actions, title = title, scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = Color.Transparent, + ), navigationIcon = { JetchatIcon( contentDescription = stringResource(id = R.string.navigation_drawer_open), diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt index 43bc0a387d..91593e0d29 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer @@ -80,6 +81,7 @@ import androidx.compose.ui.draganddrop.DragAndDropTarget import androidx.compose.ui.draganddrop.mimeTypes import androidx.compose.ui.draganddrop.toAndroidDragEvent import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale @@ -91,12 +93,17 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.example.compose.jetchat.FunctionalityNotAvailablePopup import com.example.compose.jetchat.R +import com.example.compose.jetchat.blur.BlurRadiusSpec +import com.example.compose.jetchat.blur.backdropBlur import com.example.compose.jetchat.components.JetchatAppBar import com.example.compose.jetchat.data.exampleUiState import com.example.compose.jetchat.theme.JetchatTheme +import com.example.compose.jetchat.video.FullScreenVideoPlayer +import com.example.compose.jetchat.video.VideoThumbnail import kotlinx.coroutines.launch /** @@ -190,7 +197,8 @@ fun ConversationContent( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { paddingValues -> Column( - Modifier.fillMaxSize().padding(paddingValues) + Modifier.fillMaxSize() + .padding(bottom = paddingValues.calculateBottomPadding()) .background(color = background) .border(width = 2.dp, color = borderStroke) .dragAndDropTarget(shouldStartDragAndDrop = { event -> @@ -206,6 +214,7 @@ fun ConversationContent( navigateToProfile = navigateToProfile, modifier = Modifier.weight(1f), scrollState = scrollState, + contentPadding = PaddingValues(top = paddingValues.calculateTopPadding()), onVideoClick = { videoUri -> activeVideoUri = videoUri }, ) UserInput( @@ -265,7 +274,12 @@ fun ChannelNameBar( FunctionalityNotAvailablePopup { functionalityNotAvailablePopupShown = false } } JetchatAppBar( - modifier = modifier, + modifier = modifier + .backdropBlur( + tint = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f), + elevation = 0.dp, + radius = 12.dp, + ), scrollBehavior = scrollBehavior, onNavIconPressed = onNavIconPressed, title = { @@ -316,6 +330,7 @@ fun Messages( navigateToProfile: (String) -> Unit, scrollState: LazyListState, modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), onVideoClick: (String) -> Unit = {}, ) { val scope = rememberCoroutineScope() @@ -325,6 +340,7 @@ fun Messages( LazyColumn( reverseLayout = true, state = scrollState, + contentPadding = contentPadding, modifier = Modifier .testTag(ConversationTestTag) .fillMaxSize(), @@ -338,16 +354,16 @@ fun Messages( // Hardcode day dividers for simplicity if (index == messages.size - 1) { - item { + item(key = "header_20_aug", contentType = "header") { DayHeader("20 Aug") } } else if (index == 2) { - item { + item(key = "header_today", contentType = "header") { DayHeader("Today") } } - item { + item(key = content.id, contentType = "message") { Message( onAuthorClick = { name -> navigateToProfile(name) }, msg = content, diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationUiState.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationUiState.kt index a7d8ea18c1..eaca8dd9e0 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationUiState.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationUiState.kt @@ -19,6 +19,7 @@ package com.example.compose.jetchat.conversation import androidx.compose.runtime.Immutable import androidx.compose.runtime.toMutableStateList import com.example.compose.jetchat.R +import java.util.UUID class ConversationUiState(val channelName: String, val channelMembers: Int, initialMessages: List) { private val _messages: MutableList = initialMessages.toMutableStateList() @@ -37,4 +38,5 @@ data class Message( val image: Int? = null, val authorImage: Int = if (author == "me") R.drawable.ali else R.drawable.someone_else, val videoUri: String? = null, + val id: String = UUID.randomUUID().toString(), ) diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt index fd0270ae0f..49ceb985fd 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.example.compose.jetchat.R @@ -71,7 +72,7 @@ fun JumpToBottom(enabled: Boolean, onClicked: () -> Unit, modifier: Modifier = M containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.primary, modifier = modifier - .offset(x = 0.dp, y = -bottomOffset) + .offset { IntOffset(x = 0, y = -bottomOffset.roundToPx()) } .height(36.dp), ) } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt index bcc656edd4..17701ad325 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt @@ -19,6 +19,7 @@ package com.example.compose.jetchat.conversation import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle @@ -58,42 +59,43 @@ typealias SymbolAnnotation = Pair */ @Composable fun messageFormatter(text: String, primary: Boolean): AnnotatedString { - val tokens = symbolPattern.findAll(text) - - return buildAnnotatedString { - - var cursorPosition = 0 - - val codeSnippetBackground = - if (primary) { - MaterialTheme.colorScheme.secondary - } else { - MaterialTheme.colorScheme.surface + val colorScheme = MaterialTheme.colorScheme + return remember(text, primary, colorScheme) { + val tokens = symbolPattern.findAll(text) + buildAnnotatedString { + var cursorPosition = 0 + + val codeSnippetBackground = + if (primary) { + colorScheme.secondary + } else { + colorScheme.surface + } + + for (token in tokens) { + append(text.slice(cursorPosition until token.range.first)) + + val (annotatedString, stringAnnotation) = getSymbolAnnotation( + matchResult = token, + colorScheme = colorScheme, + primary = primary, + codeSnippetBackground = codeSnippetBackground, + ) + append(annotatedString) + + if (stringAnnotation != null) { + val (item, start, end, tag) = stringAnnotation + addStringAnnotation(tag = tag, start = start, end = end, annotation = item) + } + + cursorPosition = token.range.last + 1 } - for (token in tokens) { - append(text.slice(cursorPosition until token.range.first)) - - val (annotatedString, stringAnnotation) = getSymbolAnnotation( - matchResult = token, - colorScheme = MaterialTheme.colorScheme, - primary = primary, - codeSnippetBackground = codeSnippetBackground, - ) - append(annotatedString) - - if (stringAnnotation != null) { - val (item, start, end, tag) = stringAnnotation - addStringAnnotation(tag = tag, start = start, end = end, annotation = item) + if (!tokens.none()) { + append(text.slice(cursorPosition..text.lastIndex)) + } else { + append(text) } - - cursorPosition = token.range.last + 1 - } - - if (!tokens.none()) { - append(text.slice(cursorPosition..text.lastIndex)) - } else { - append(text) } } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt index 699b3835c5..a61148c79d 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt @@ -110,6 +110,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.compose.jetchat.FunctionalityNotAvailablePopup import com.example.compose.jetchat.R +import com.example.compose.jetchat.video.VideoPlayer import kotlin.math.absoluteValue import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/data/FakeData.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/data/FakeData.kt index ad724a1438..ce0020e2bd 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/data/FakeData.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/data/FakeData.kt @@ -84,6 +84,156 @@ val initialMessages = listOf( "Yeah its seems to be pretty new!", "8:12 PM", ), + Message( + "me", + "Speaking of sweets, check out the cupcakes from the Android release party! 🧁", + "8:15 PM", + R.drawable.cupcake, + ), + Message( + "Taylor Brooks", + "Those look amazing! Reminds me of the classic Donut days 🍩", + "8:16 PM", + R.drawable.donut, + ), + Message( + "John Glenn", + "Donut (1.6) was iconic! But Eclair brought live wallpapers and turn-by-turn navigation πŸ—ΊοΈ", + "8:18 PM", + R.drawable.eclair, + ), + Message( + "Shangeeth Sivan", + "Froyo was a massive leap forward with JIT compilation ⚑", + "8:20 PM", + R.drawable.froyo, + ), + Message( + "me", + "And who could forget the gingerbread man statue on the Google lawn? πŸͺ", + "8:22 PM", + R.drawable.gingerbread, + ), + Message( + "Taylor Brooks", + "Honeycomb 3.0 was dedicated entirely to tablets! Check out this statue 🐝", + "8:25 PM", + R.drawable.honeycomb, + ), + Message( + "John Glenn", + "Ice Cream Sandwich (4.0) unified phones and tablets with Holo design! 🍦πŸ₯ͺ", + "8:27 PM", + R.drawable.ice_cream_sandwich, + ), + Message( + "Shangeeth Sivan", + "Jelly Bean introduced Project Butter to guarantee smooth 60fps animations 🧈", + "8:30 PM", + R.drawable.jelly_bean, + ), + Message( + "me", + "Have a break, have a KitKat! 🍫 Translucent system bars started right here.", + "8:32 PM", + R.drawable.kitkat, + ), + Message( + "Taylor Brooks", + "Lollipop (5.0) brought Material Design 1.0 into the world! Look at this statue 🍭", + "8:35 PM", + R.drawable.lollipop, + ), + Message( + "John Glenn", + "Marshmallow (6.0) added runtime permissions and Doze battery mode ☁️", + "8:38 PM", + R.drawable.marshmallow, + ), + Message( + "Shangeeth Sivan", + "Nougat (7.0) gave us multi-window split screen and Vulkan graphics support! πŸ“±", + "8:40 PM", + R.drawable.nougat, + ), + Message( + "me", + "Oreo was one of the coolest statues at the Googleplex πŸͺ", + "8:42 PM", + R.drawable.oreo, + ), + Message( + "Taylor Brooks", + "And Pie (9.0) with gesture navigation and adaptive battery πŸ₯§", + "8:45 PM", + R.drawable.pie, + ), + Message( + "John Glenn", + "Homemade apple pie from the team potluck today! 🍎πŸ₯§", + "8:48 PM", + R.drawable.apple_pie, + ), + Message( + "Shangeeth Sivan", + "Micro-kitchen is fully restocked for the hackathon! πŸ₯¨", + "8:50 PM", + R.drawable.pretzels, + ), + Message( + "me", + "Smoothie break before the design review πŸ“πŸ₯", + "8:52 PM", + R.drawable.smoothies, + ), + Message( + "Taylor Brooks", + "Fresh fruit delivered to building 43! πŸ‡πŸŽ", + "8:55 PM", + R.drawable.fruit, + ), + Message( + "John Glenn", + "Cheese board ready for the I/O watch party πŸ§€", + "8:58 PM", + R.drawable.cheese, + ), + Message( + "Shangeeth Sivan", + "Got chips and salsa too! πŸ₯‘", + "9:00 PM", + R.drawable.chips, + ), + Message( + "me", + "Popcorn is popping for the amphitheater demo 🍿", + "9:02 PM", + R.drawable.popcorn, + ), + Message( + "Taylor Brooks", + "Apple chips for the healthy snackers 🍏", + "9:05 PM", + R.drawable.apple_chips, + ), + Message( + "John Glenn", + "Fresh almonds too! 🌰", + "9:08 PM", + R.drawable.almonds, + ), + Message( + "Shangeeth Sivan", + "Apple juice is chilled in the fridge πŸ§ƒ", + "9:10 PM", + R.drawable.apple_juice, + ), + Message( + "me", + "Desserts galore! This team definitely loves Android snacks πŸŽ‰", + "9:15 PM", + R.drawable.desserts, + ), ) val unreadMessages = initialMessages.filter { it.author != "me" } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/VideoPlayer.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/video/VideoPlayer.kt similarity index 98% rename from Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/VideoPlayer.kt rename to Jetchat/app/src/main/java/com/example/compose/jetchat/video/VideoPlayer.kt index 69f68520b8..c40df55c2a 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/VideoPlayer.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/video/VideoPlayer.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.compose.jetchat.conversation +package com.example.compose.jetchat.video import android.graphics.Bitmap import android.graphics.RectF @@ -68,7 +68,6 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect -import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi @@ -79,6 +78,8 @@ import androidx.media3.ui.compose.state.rememberPlayPauseButtonState import androidx.media3.ui.compose.state.rememberPresentationState import androidx.media3.ui.compose.state.rememberProgressStateWithTickInterval import com.example.compose.jetchat.R +import com.example.compose.jetchat.conversation.BlurRegionSpec +import com.example.compose.jetchat.conversation.SurfaceViewBlurHelper import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/VideoPlayerControls.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/video/VideoPlayerControls.kt similarity index 98% rename from Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/VideoPlayerControls.kt rename to Jetchat/app/src/main/java/com/example/compose/jetchat/video/VideoPlayerControls.kt index fe63011552..ebfc0acc33 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/VideoPlayerControls.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/video/VideoPlayerControls.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.compose.jetchat.conversation +package com.example.compose.jetchat.video import androidx.annotation.OptIn import androidx.compose.foundation.background @@ -58,6 +58,8 @@ import androidx.media3.ui.compose.state.MuteButtonState import androidx.media3.ui.compose.state.PlayPauseButtonState import androidx.media3.ui.compose.state.ProgressStateWithTickInterval import com.example.compose.jetchat.R +import com.example.compose.jetchat.conversation.BlurRegionSpec +import com.example.compose.jetchat.conversation.registerBlurRegion import java.util.Locale /** diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/VideoPlayerWindowHelper.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/video/VideoPlayerWindowHelper.kt similarity index 97% rename from Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/VideoPlayerWindowHelper.kt rename to Jetchat/app/src/main/java/com/example/compose/jetchat/video/VideoPlayerWindowHelper.kt index c34d262770..29e0bd839d 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/VideoPlayerWindowHelper.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/video/VideoPlayerWindowHelper.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.compose.jetchat.conversation +package com.example.compose.jetchat.video import android.app.Activity import android.content.ContextWrapper diff --git a/Jetchat/app/src/main/res/drawable-nodpi/almonds.jpg b/Jetchat/app/src/main/res/drawable-nodpi/almonds.jpg new file mode 100644 index 0000000000..0f1eef57e3 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/almonds.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/apple_chips.jpg b/Jetchat/app/src/main/res/drawable-nodpi/apple_chips.jpg new file mode 100644 index 0000000000..37c805d169 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/apple_chips.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/apple_juice.jpg b/Jetchat/app/src/main/res/drawable-nodpi/apple_juice.jpg new file mode 100644 index 0000000000..d519dbcd46 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/apple_juice.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/apple_pie.jpg b/Jetchat/app/src/main/res/drawable-nodpi/apple_pie.jpg new file mode 100644 index 0000000000..41096b2216 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/apple_pie.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/apple_sauce.jpg b/Jetchat/app/src/main/res/drawable-nodpi/apple_sauce.jpg new file mode 100644 index 0000000000..7f5331ee24 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/apple_sauce.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/apples.jpg b/Jetchat/app/src/main/res/drawable-nodpi/apples.jpg new file mode 100644 index 0000000000..cc729f1646 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/apples.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/cheese.jpg b/Jetchat/app/src/main/res/drawable-nodpi/cheese.jpg new file mode 100644 index 0000000000..c5c6dce61c Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/cheese.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/chips.jpg b/Jetchat/app/src/main/res/drawable-nodpi/chips.jpg new file mode 100644 index 0000000000..04d5fe2cd3 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/chips.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/cupcake.jpg b/Jetchat/app/src/main/res/drawable-nodpi/cupcake.jpg new file mode 100644 index 0000000000..42e766d843 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/cupcake.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/desserts.jpg b/Jetchat/app/src/main/res/drawable-nodpi/desserts.jpg new file mode 100644 index 0000000000..6d44990765 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/desserts.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/donut.jpg b/Jetchat/app/src/main/res/drawable-nodpi/donut.jpg new file mode 100644 index 0000000000..076896a812 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/donut.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/eclair.jpg b/Jetchat/app/src/main/res/drawable-nodpi/eclair.jpg new file mode 100644 index 0000000000..5601780345 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/eclair.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/froyo.jpg b/Jetchat/app/src/main/res/drawable-nodpi/froyo.jpg new file mode 100644 index 0000000000..e1bb068cc9 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/froyo.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/fruit.jpg b/Jetchat/app/src/main/res/drawable-nodpi/fruit.jpg new file mode 100644 index 0000000000..4122473184 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/fruit.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/gingerbread.jpg b/Jetchat/app/src/main/res/drawable-nodpi/gingerbread.jpg new file mode 100644 index 0000000000..ac069de103 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/gingerbread.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/gluten_free.jpg b/Jetchat/app/src/main/res/drawable-nodpi/gluten_free.jpg new file mode 100644 index 0000000000..0745457a36 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/gluten_free.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/grapes.jpg b/Jetchat/app/src/main/res/drawable-nodpi/grapes.jpg new file mode 100644 index 0000000000..3b573787cf Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/grapes.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/honeycomb.jpg b/Jetchat/app/src/main/res/drawable-nodpi/honeycomb.jpg new file mode 100644 index 0000000000..ea632bd25d Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/honeycomb.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/ice_cream_sandwich.jpg b/Jetchat/app/src/main/res/drawable-nodpi/ice_cream_sandwich.jpg new file mode 100644 index 0000000000..fd77631e9a Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/ice_cream_sandwich.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/jelly_bean.jpg b/Jetchat/app/src/main/res/drawable-nodpi/jelly_bean.jpg new file mode 100644 index 0000000000..84a10208c9 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/jelly_bean.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/kitkat.jpg b/Jetchat/app/src/main/res/drawable-nodpi/kitkat.jpg new file mode 100644 index 0000000000..75b2e44abb Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/kitkat.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/kiwi.jpg b/Jetchat/app/src/main/res/drawable-nodpi/kiwi.jpg new file mode 100644 index 0000000000..2197fbd48f Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/kiwi.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/lollipop.jpg b/Jetchat/app/src/main/res/drawable-nodpi/lollipop.jpg new file mode 100644 index 0000000000..98d1db7d2f Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/lollipop.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/mango.jpg b/Jetchat/app/src/main/res/drawable-nodpi/mango.jpg new file mode 100644 index 0000000000..717773d7b5 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/mango.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/marshmallow.jpg b/Jetchat/app/src/main/res/drawable-nodpi/marshmallow.jpg new file mode 100644 index 0000000000..cdc1159226 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/marshmallow.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/nougat.jpg b/Jetchat/app/src/main/res/drawable-nodpi/nougat.jpg new file mode 100644 index 0000000000..1a844d9b9f Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/nougat.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/nuts.jpg b/Jetchat/app/src/main/res/drawable-nodpi/nuts.jpg new file mode 100644 index 0000000000..03556767ec Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/nuts.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/oreo.jpg b/Jetchat/app/src/main/res/drawable-nodpi/oreo.jpg new file mode 100644 index 0000000000..cf2c3e534c Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/oreo.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/organic.jpg b/Jetchat/app/src/main/res/drawable-nodpi/organic.jpg new file mode 100644 index 0000000000..2847abf4f0 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/organic.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/paleo.jpg b/Jetchat/app/src/main/res/drawable-nodpi/paleo.jpg new file mode 100644 index 0000000000..750fcd58e7 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/paleo.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/pie.jpg b/Jetchat/app/src/main/res/drawable-nodpi/pie.jpg new file mode 100644 index 0000000000..439c18cfbb Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/pie.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/placeholder.jpg b/Jetchat/app/src/main/res/drawable-nodpi/placeholder.jpg new file mode 100644 index 0000000000..31e05faacb Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/placeholder.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/popcorn.jpg b/Jetchat/app/src/main/res/drawable-nodpi/popcorn.jpg new file mode 100644 index 0000000000..02713ffdbf Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/popcorn.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/pretzels.jpg b/Jetchat/app/src/main/res/drawable-nodpi/pretzels.jpg new file mode 100644 index 0000000000..d31d4aa4eb Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/pretzels.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/smoothies.jpg b/Jetchat/app/src/main/res/drawable-nodpi/smoothies.jpg new file mode 100644 index 0000000000..f2eaa31bf1 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/smoothies.jpg differ diff --git a/Jetchat/app/src/main/res/drawable-nodpi/vegan.jpg b/Jetchat/app/src/main/res/drawable-nodpi/vegan.jpg new file mode 100644 index 0000000000..29276a68b7 Binary files /dev/null and b/Jetchat/app/src/main/res/drawable-nodpi/vegan.jpg differ diff --git a/Jetchat/gradle/libs.versions.toml b/Jetchat/gradle/libs.versions.toml index b92422ef02..16fb0c3ae2 100644 --- a/Jetchat/gradle/libs.versions.toml +++ b/Jetchat/gradle/libs.versions.toml @@ -8,7 +8,7 @@ android-material3 = "1.14.0" androidGradlePlugin = "9.3.1" androidx-activity-compose = "1.13.0" androidx-appcompat = "1.8.0" -androidx-compose-bom = "2026.08.00" +androidx-compose-bom = "2026.08.01" androidx-constraintlayout = "1.1.2" androidx-core-splashscreen = "1.2.0" androidx-corektx = "1.19.0" @@ -48,7 +48,7 @@ maps-compose = "8.4.0" media3Exoplayer = "1.11.0" media3ExoplayerHls = "1.11.0" media3UiCompose = "1.11.0" -minSdk = "23" +minSdk = "24" okhttp = "5.4.0" play-services-wearable = "20.0.1" robolectric = "4.16.1" @@ -69,7 +69,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity-compose" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-compose-animation = { module = "androidx.compose.animation:animation" } -androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } +androidx-compose-bom = { module = "androidx.compose:compose-bom-alpha", version.ref = "androidx-compose-bom" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" }