Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a6fda72
feat(chat): record video messages from the message input
ToteMeiSter Oct 4, 2026
6186c74
fix(chat): start recording only on hold and show the record hint as a…
ToteMeiSter Oct 4, 2026
ae71b02
fix(chat): keep a video recording over a screen rotation
ToteMeiSter Oct 4, 2026
64fbec6
fix(chat): block taps and TalkBack focus on the video recording preview
ToteMeiSter Oct 4, 2026
151a7ce
fix(chat): measure the minimum voice message length from the recordin…
ToteMeiSter Oct 5, 2026
032345c
fix(chat): release a locked video recording only when CameraX has fin…
ToteMeiSter Oct 5, 2026
24d4527
fix(chat): dim the chat and fit the video message preview to the reco…
ToteMeiSter Oct 5, 2026
9bd50e4
fix(chat): center the video preview by gravity, follow the screen ori…
ToteMeiSter Oct 5, 2026
950eb2a
fix(chat): one-line recording panel for video messages, larger preview
ToteMeiSter Oct 5, 2026
10660fb
fix(chat): video recording row review fixes
ToteMeiSter Oct 5, 2026
251213a
fix(chat): defer video result until chat is loaded, video a11y labels
ToteMeiSter Oct 5, 2026
1687ca2
fix(chat): harden video recording against review findings
ToteMeiSter Oct 18, 2026
881260d
fix(chat): keep the locked recording panel when the chat finishes loa…
ToteMeiSter Oct 18, 2026
1f46b9a
fix(chat): do not ask for media access before the camera photo preview
ToteMeiSter Oct 18, 2026
4fddff2
fix(chat): do not let the record button click fire after the release
ToteMeiSter Oct 18, 2026
86493e1
fix(chat): wait for the music of another app to stop before a video m…
ToteMeiSter Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ val androidxTestVersion = "1.5.0"
val media3Version = "1.11.1"
val coroutinesVersion = "1.11.0"
val mockitoKotlinVersion = "6.4.0"
val cameraXVersion = "1.6.1"

android {
compileSdk = 37
Expand Down Expand Up @@ -244,6 +245,12 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion")

implementation("androidx.camera:camera-core:$cameraXVersion")
implementation("androidx.camera:camera-camera2:$cameraXVersion")
implementation("androidx.camera:camera-lifecycle:$cameraXVersion")
implementation("androidx.camera:camera-video:$cameraXVersion")
implementation("androidx.camera:camera-view:$cameraXVersion")

implementation("androidx.biometric:biometric:1.1.0")

implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
Expand Down
60 changes: 60 additions & 0 deletions app/src/main/java/com/nextcloud/talk/chat/CameraLens.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.chat

import android.content.Context
import android.util.Log
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.UseCase
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import java.util.concurrent.ExecutionException

private const val TAG = "CameraLens"

internal fun cameraSelectorFor(lens: Int): CameraSelector = CameraSelector.Builder().requireLensFacing(lens).build()

internal fun oppositeLens(lens: Int): Int =
if (lens == CameraSelector.LENS_FACING_FRONT) CameraSelector.LENS_FACING_BACK else CameraSelector.LENS_FACING_FRONT

/**
* The lens to bind: [preferred] when the device has it, otherwise the other one.
*/
internal fun resolveLens(provider: ProcessCameraProvider, preferred: Int): Int =
if (provider.hasCamera(cameraSelectorFor(preferred))) preferred else oppositeLens(preferred)

/**
* Calls [onResult] on the main thread with the camera provider, or with null when it is not available.
*/
internal fun whenCameraProviderReady(context: Context, onResult: (ProcessCameraProvider?) -> Unit) {
val providerFuture = ProcessCameraProvider.getInstance(context)
providerFuture.addListener({
val provider = try {
providerFuture.get()
} catch (e: ExecutionException) {
Log.w(TAG, "camera provider is not available", e)
null
}
onResult(provider)
}, ContextCompat.getMainExecutor(context))
}

/**
* Binds [useCases] to the camera with [lens]. Returns null when the camera cannot be bound.
*/
internal fun ProcessCameraProvider.bindSafely(owner: LifecycleOwner, lens: Int, vararg useCases: UseCase): Camera? =
try {
bindToLifecycle(owner, cameraSelectorFor(lens), *useCases)
} catch (e: IllegalArgumentException) {
Log.w(TAG, "cannot bind camera use cases", e)
null
} catch (e: IllegalStateException) {
Log.w(TAG, "cannot bind camera use cases", e)
null
}
Loading
Loading