diff --git a/.github/workflows/JetLagged.yaml b/.github/workflows/JetLagged.yaml index f7ee31f8d4..1068a7bfcb 100644 --- a/.github/workflows/JetLagged.yaml +++ b/.github/workflows/JetLagged.yaml @@ -38,13 +38,18 @@ jobs: timeout-minutes: 30 strategy: matrix: - api-level: [24, 26] + api-level: [30, 31] steps: - name: Checkout uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm - name: Copy CI gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties @@ -68,9 +73,11 @@ jobs: - name: Run instrumentation tests uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 + env: + ADB_INSTALL_TIMEOUT: 10 with: api-level: ${{ matrix.api-level }} - arch: x86 + arch: x86_64 disable-animations: true script: ./gradlew connectedCheck --stacktrace working-directory: ${{ env.SAMPLE_PATH }} diff --git a/.github/workflows/JetNews.yaml b/.github/workflows/JetNews.yaml index 90fa3765ce..5656a11c10 100644 --- a/.github/workflows/JetNews.yaml +++ b/.github/workflows/JetNews.yaml @@ -38,7 +38,7 @@ jobs: timeout-minutes: 30 strategy: matrix: - api-level: [24, 26] + api-level: [30, 31] steps: - name: Checkout @@ -71,9 +71,11 @@ jobs: key: gradle-${{ hashFiles('checksum.txt') }} - name: Run instrumentation tests uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 + env: + ADB_INSTALL_TIMEOUT: 10 with: api-level: ${{ matrix.api-level }} - arch: x86 + arch: x86_64 disable-animations: true script: ./gradlew connectedCheck --stacktrace working-directory: ${{ env.SAMPLE_PATH }} diff --git a/.github/workflows/Jetchat.yaml b/.github/workflows/Jetchat.yaml index da3b6dc555..4995ef3ed6 100644 --- a/.github/workflows/Jetchat.yaml +++ b/.github/workflows/Jetchat.yaml @@ -37,13 +37,18 @@ jobs: timeout-minutes: 30 strategy: matrix: - api-level: [24, 26] + api-level: [30, 31] steps: - name: Checkout uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm - name: Copy CI gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties @@ -67,9 +72,11 @@ jobs: - name: Run instrumentation tests uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 + env: + ADB_INSTALL_TIMEOUT: 10 with: api-level: ${{ matrix.api-level }} - arch: x86 + arch: x86_64 disable-animations: true script: ./gradlew connectedCheck --stacktrace working-directory: ${{ env.SAMPLE_PATH }} diff --git a/.github/workflows/Reply.yaml b/.github/workflows/Reply.yaml index 2cd3ae64cd..ac678835b8 100644 --- a/.github/workflows/Reply.yaml +++ b/.github/workflows/Reply.yaml @@ -38,13 +38,18 @@ jobs: timeout-minutes: 30 strategy: matrix: - api-level: [24, 26] + api-level: [30, 31] steps: - name: Checkout uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm - name: Copy CI gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties @@ -68,9 +73,11 @@ jobs: - name: Run instrumentation tests uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 + env: + ADB_INSTALL_TIMEOUT: 10 with: api-level: ${{ matrix.api-level }} - arch: x86 + arch: x86_64 disable-animations: true script: ./gradlew connectedCheck --stacktrace working-directory: ${{ env.SAMPLE_PATH }} diff --git a/Jetchat/app/build.gradle.kts b/Jetchat/app/build.gradle.kts index 4d325b2b74..3197802b0b 100644 --- a/Jetchat/app/build.gradle.kts +++ b/Jetchat/app/build.gradle.kts @@ -24,6 +24,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = 2 namespace = "com.example.compose.jetchat" defaultConfig { 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..5d1937a9a9 --- /dev/null +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BaseBackdropNode.kt @@ -0,0 +1,246 @@ +/* + * 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 + } + + // Suppress NewApi: Android Lint does not yet recognize Build.VERSION.SDK_INT_FULL + // checks as satisfying minor SDK 37.2 requirements. + @SuppressLint("NewApi") + 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/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..d53ad941c1 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 @@ -37,4 +37,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 = "$author-$timestamp-$content", ) 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..f5e80315d1 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" @@ -58,7 +58,7 @@ room = "2.8.4" secrets = "2.0.1" spotless = "8.9.0" # @keep -targetSdk = "33" +targetSdk = "37" version-catalog-update = "1.1.1" [libraries] @@ -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" }