diff --git a/app/src/main/java/com/android/developers/androidify/MainActivity.kt b/app/src/main/java/com/android/developers/androidify/MainActivity.kt index 9d2e5e24..da4e9a37 100644 --- a/app/src/main/java/com/android/developers/androidify/MainActivity.kt +++ b/app/src/main/java/com/android/developers/androidify/MainActivity.kt @@ -17,6 +17,7 @@ package com.android.developers.androidify import android.os.Build import android.os.Bundle +import android.view.KeyEvent import android.view.WindowManager import android.window.TrustedPresentationThresholds import androidx.activity.ComponentActivity @@ -28,6 +29,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import com.android.developers.androidify.camera.HardwareKeyManager import com.android.developers.androidify.navigation.MainNavigation import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.util.LocalOcclusion @@ -94,4 +96,12 @@ class MainActivity : ComponentActivity() { windowManager.unregisterTrustedPresentationListener(presentationListener) } } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean = + if (HardwareKeyManager.dispatchDown(keyCode, event)) true + else super.onKeyDown(keyCode, event) + + override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean = + if (HardwareKeyManager.dispatchUp(keyCode, event)) true + else super.onKeyUp(keyCode, event) } diff --git a/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraCaptureButton.kt b/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraCaptureButton.kt index c49bb074..9a4d78f2 100644 --- a/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraCaptureButton.kt +++ b/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraCaptureButton.kt @@ -15,6 +15,7 @@ */ package com.android.developers.androidify.camera +import android.view.KeyEvent import androidx.compose.foundation.clickable import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource @@ -28,7 +29,10 @@ import androidx.compose.material3.ripple import androidx.compose.material3.toPath import androidx.compose.material3.toShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithCache @@ -86,7 +90,6 @@ internal fun CameraCaptureButton( } else { stringResource(R.string.capture_image_button_disabled_content_description) } - Spacer( modifier .indication(interactionSource, ScaleIndicationNodeFactory(animationSpec)) @@ -146,3 +149,40 @@ internal fun CameraCaptureButton( }, ) } + +@Composable +internal fun RegisterHardwareShutter( + enabled: Boolean, + onCapture: () -> Unit, +) { + val currentCapture by rememberUpdatedState(newValue = onCapture) + + DisposableEffect(enabled) { + val token = HardwareKeyManager.register(object : HardwareKeyManager.Handler { + override val priority = 10 + + private fun isShutterKey(keyCode: Int): Boolean { + return when (keyCode) { + KeyEvent.KEYCODE_VOLUME_UP, + KeyEvent.KEYCODE_VOLUME_DOWN, + KeyEvent.KEYCODE_CAMERA -> true + else -> false + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (enabled && event.repeatCount == 0 && isShutterKey(keyCode)) { + currentCapture() + return true + } + return false + } + + override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + return enabled && isShutterKey(keyCode) + } + + }) + onDispose { token.close() } + } +} diff --git a/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraScreen.kt b/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraScreen.kt index 80b649fd..7c4a5f87 100644 --- a/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraScreen.kt +++ b/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraScreen.kt @@ -226,6 +226,10 @@ fun StatelessCameraPreviewContent( enabled = detectedPose, captureImageClicked = requestCaptureImage, ) + RegisterHardwareShutter( + enabled = detectedPose, + onCapture = requestCaptureImage, + ) }, flipCameraButton = { flipModifier -> if (canFlipCamera) { diff --git a/feature/camera/src/main/java/com/android/developers/androidify/camera/HardwareKeyManager.kt b/feature/camera/src/main/java/com/android/developers/androidify/camera/HardwareKeyManager.kt new file mode 100644 index 00000000..a1a0e80d --- /dev/null +++ b/feature/camera/src/main/java/com/android/developers/androidify/camera/HardwareKeyManager.kt @@ -0,0 +1,37 @@ +package com.android.developers.androidify.camera + +import android.view.KeyEvent +import java.util.concurrent.CopyOnWriteArrayList + +object HardwareKeyManager { + interface Handler { + /** Higher number = higher priority */ + val priority: Int get() = 0 + fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean = false + fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean = false + } + + private val handlers = mutableListOf() + + fun register(handler: Handler): AutoCloseable { + synchronized(handlers) { + handlers.add(handler) + handlers.sortByDescending { it.priority } + } + return AutoCloseable { + synchronized(handlers) { + handlers.remove(handler) + } + } + } + + fun dispatchDown(keyCode: Int, event: KeyEvent): Boolean { + val handlersCopy = synchronized(handlers) { handlers.toList() } + return handlersCopy.any { it.onKeyDown(keyCode, event) } + } + + fun dispatchUp(keyCode: Int, event: KeyEvent): Boolean { + val handlersCopy = synchronized(handlers) { handlers.toList() } + return handlersCopy.any { it.onKeyUp(keyCode, event) } + } +} \ No newline at end of file