Basic Usage
How to integrate the camera UI with CameraView and CameraController.
Before You Begin
Before starting this guide, complete Getting Started. You need to prepare your license (LicenseInfo) and detection settings (DetectionConfiguration).
The recommended integration approach for 1.4.0 is to use CameraView (a FrameLayout widget that draws the preview plus the default tracking UI) and CameraController (the camera/detection session engine) directly. Because it does not require subclassing a Fragment, it integrates naturally into plain View screens, Jetpack Compose, and custom UIs alike.
If you are already using the Fragment hosting approach (PetnowCameraFragment), see the Fragment Approach (Legacy) document.
Place the camera view
Add CameraView to your layout or Compose.
Create and connect the controller
Create a CameraController, register a listener, then connect it to CameraView.
Start the session
Check the camera permission and call initializeCamera() → startDetection().
Handle results
Handle the capture-completed/failed callbacks.
Clean up resources
Clean up the controller when the screen is dismissed.
Step 1: Place the Camera View
CameraView is a widget that extends FrameLayout. Add it directly to an XML layout, or wrap it with AndroidView in Compose.
XML layout
<io.petnow.ui.CameraView
android:id="@+id/camera_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />Jetpack Compose
import androidx.compose.ui.viewinterop.AndroidView
import io.petnow.ui.CameraView
AndroidView(
factory = { ctx -> CameraView(ctx) },
modifier = Modifier.fillMaxSize(),
update = { view -> view.controller = controller }, // The controller from Step 2
)CameraView automatically renders the camera preview and the default tracking UI (nose/face trackers). It also manages the preview Surface internally, so all you need to do is connect a controller.
Step 2: Create and Connect the Controller
Create a CameraController, register a detection event listener, and then connect it to CameraView.
import io.petnow.ui.CameraController
import io.petnow.ui.CameraResult
import io.petnow.ui.config.LicenseInfo
import io.petnow.ui.status.DetectionStatus
import io.petnow.callback.PetnowCameraDetectionListenerV2
// Use a CoroutineScope tied to the screen lifecycle, such as lifecycleScope.
val controller = CameraController(
context.applicationContext,
LicenseInfo(apiKey = "YOUR_API_KEY", isDebugMode = false),
lifecycleScope,
)
controller.setDetectionListenerV2(object : PetnowCameraDetectionListenerV2 {
override fun onDetectionStatus(primaryDetectionStatus: DetectionStatus) {
// Implemented in Step 5 — real-time guidance
}
override fun onDetectionProgress(progress: Int) {
// Implemented in Step 5 — progress 0–100
}
override fun onDetectionFinished(result: CameraResult) {
// Implemented in Step 4 — final result
}
})
// Connecting it to CameraView starts the preview.
cameraView.controller = controller Understanding the Elements
| Element | Description |
|---|---|
CameraController(context, license, scope) | The camera/detection session engine. scope is a CoroutineScope (e.g. lifecycleScope) that stays valid for the duration of the session |
LicenseInfo(apiKey, isDebugMode) | License information (for monitoring/metrics). isDebugMode should be false |
setDetectionListenerV2(...) | Registers callbacks for detection status, progress, and the final result |
cameraView.controller = ... | Connects the controller to the view (automatically supplies the preview Surface) |
Detection settings (species/purpose/enableFakeDetection) are passed via DetectionConfiguration in the next step's initializeCamera(), not at controller creation.
Step 3: Start the Session
After checking the camera permission, prepare the session with initializeCamera() and start detection with startDetection(). The two calls are separate — initializeCamera() only opens the camera, and detection does not begin until you call startDetection().
import io.petnow.ui.config.DetectionConfiguration
import io.petnow.ui.config.DetectionPurpose
import io.petnow.ui.config.PetSpecies
import java.util.UUID
val configuration = DetectionConfiguration(
species = PetSpecies.DOG,
purpose = DetectionPurpose.PET_PROFILE_REGISTRATION,
enableFakeDetection = true,
)
// captureSessionId issued by the server (see Getting Started)
val captureSessionId: UUID = /* captureSessionId received from the server */
lifecycleScope.launch {
// CameraView does not request permissions itself — the host must check first.
if (!hasCameraPermission()) {
requestCameraPermission()
return@launch
}
controller.initializeCamera(configuration, captureSessionId)
controller.startDetection()
}The host handles permissions
Unlike PetnowCameraFragment, CameraView does not automatically request the camera permission. Declare <uses-permission android:name="android.permission.CAMERA" /> in AndroidManifest.xml and request the runtime permission yourself before starting the session.
initializeCamera() is a suspend function that returns only after the preview Surface is ready and the camera has opened for the first time. The startDetection() call immediately afterward is therefore always safe.
Step 4: Handle Capture Results
When detection completes, onDetectionFinished(result: CameraResult) is called.
override fun onDetectionFinished(result: CameraResult) {
when (result) {
is CameraResult.Success -> {
// result.fingerprintImageFiles: biometric images (dog = nose, cat = face)
// result.appearanceImageFiles: appearance (face) images
uploadImages(result.fingerprintImageFiles, result.appearanceImageFiles)
}
is CameraResult.Fail -> {
// Capture failed — retry or end the session
showRetryOrExitDialog()
}
}
}The CameraResult Type
sealed class CameraResult {
data class Success(
val fingerprintImageFiles: List<File>, // biometric (dog = nose, cat = face)
val appearanceImageFiles: List<File> // appearance (face) images
) : CameraResult()
data object Fail : CameraResult()
}Image files are saved to the app's cache directory under file:// paths. The recommended flow is to upload these files to your app server, which then performs registration/verification/identification via the Petnow Server API. For the upload endpoints (/v2/fingerprints:upload, /v2/appearances:upload) and the full flow, see Server API – Biometric.
Handling Failure (Fail)
CameraResult.Fail is delivered when not enough images could be captured within the capture time window (e.g. the pet left the frame, lighting was inadequate, etc.).
is CameraResult.Fail -> {
AlertDialog.Builder(requireContext())
.setTitle("Capture failed")
.setMessage("We couldn't capture the nose print clearly. Would you like to try again?")
.setPositiveButton("Retry") { _, _ ->
controller.startDetection() // Restart from the beginning in the same session
}
.setNegativeButton("Cancel") { _, _ -> navigateBack() }
.show()
}Server session management on failure
Even after receiving Fail, the server's capture session is still open. If you close the screen without retrying, the server automatically ends (ABORTED) the session after about 5 minutes.
Step 5: Observe Status and Progress
Progress
override fun onDetectionProgress(progress: Int) {
// progress: 0–100
progressBar.progress = progress
}Detection Status (Real-Time Guidance)
The DetectionStatus delivered through onDetectionStatus is a sealed class. When it is Failed, reason tells you the specific cause.
override fun onDetectionStatus(primaryDetectionStatus: DetectionStatus) {
statusTextView.text = when (primaryDetectionStatus) {
DetectionStatus.NoObject -> "Fit your pet within the frame"
DetectionStatus.Processing -> "Detecting..."
DetectionStatus.Detected -> "Great! Hold still"
DetectionStatus.Finished -> "Capture complete"
is DetectionStatus.Failed -> when (primaryDetectionStatus.reason) {
DetectionFailureReason.TooClose -> "Move a little farther away"
DetectionFailureReason.TooFarAway -> "Move a little closer"
DetectionFailureReason.TooDark -> "Move to a brighter spot"
DetectionFailureReason.NotFrontFace -> "Point the face straight at the camera"
else -> "Adjust the pose"
}
}
}Observing via StateFlow (Optional)
Instead of callbacks, you can also observe state via the StateFlow<DetectionState> returned by getState(). This is useful for Compose or unidirectional data flow.
lifecycleScope.launch {
controller.state.collect { state ->
// state.progressPercent: progress (0–100)
// state.detectionStatusList: list of detection statuses for the current frame
// state.currentDetectionResult: nose/face BoundingBox (DetectionResult?)
// state.isDetectionFinished: whether detection is complete
}
}The nose/face position of a detection result (DetectionResult = nose/face BoundingBox) is provided through state.currentDetectionResult, not the V2 listener.
Step 6: Clean Up Resources
Clean up the controller when the screen is dismissed. finalizeCamera() stops the camera session (the controller can be reused), while release() fully releases native resources (detector/sound pool) as well (terminates).
override fun onDestroy() {
super.onDestroy()
controller.release() // When leaving the screen for good
}In Compose, clean up in onDispose of a DisposableEffect.
DisposableEffect(Unit) {
onDispose { controller.release() }
}Additional Features
Switching Between Front/Back Cameras
import io.petnow.ui.camera.getCameraInfo
if (context.getCameraInfo().isFrontCameraSupported) {
controller.switchCamera() // Returns a Result
.onFailure { e -> Toast.makeText(context, "Switch failed: ${e.message}", Toast.LENGTH_SHORT).show() }
} else {
Toast.makeText(context, "This device does not support a front camera", Toast.LENGTH_SHORT).show()
}Pausing / Resuming Detection
You can pause detection briefly while keeping the camera preview running. This is useful while displaying a tip dialog.
controller.pauseDetection() // Pause (keeps progress/captured images, returns nothing)
controller.resumeDetection() // Resume from where it was paused (returns a Result)Re-Capturing (From the Beginning)
controller.startDetection() // Reset progress to 0 and start a new detection session (returns a Result)Differences between startDetection / pauseDetection / resumeDetection
startDetection()— Resets progress to 0 and starts a new detection session (re-capture). Returns aResult.pauseDetection()— Pauses detection only. Keeps the preview/progress. Returns nothing.resumeDetection()— Resumes from where it was paused. Returns aResult.
Best Practices
Managing the API Key Safely
// ❌ Bad: hardcoded in code
val apiKey = "sk_live_abc123..."
// ✅ Good: use BuildConfig or local.properties
val apiKey = BuildConfig.PETNOW_API_KEYDo not write the API key directly in your source code. We recommend injecting it via local.properties or CI/CD environment variables.
Cleaning Up Resources
Always call controller.release() when leaving the screen. If you don't, the camera and native detector resources won't be released, which can cause problems in the next session.
Troubleshooting
Q. The camera won't start
A. Check the following:
- Whether the camera permission (
CAMERA) is declared inAndroidManifest.xml - Whether you requested the runtime permission before starting the session (
CameraViewdoes not request it itself) - Whether you connected the controller with
cameraView.controller = controller - Whether you called
startDetection()afterinitializeCamera()
Q. Capture keeps failing
A. Review the capture environment:
- Capture under bright indoor lighting (avoid direct sunlight and shadows)
- Keep a distance of 30–50 cm between the camera and the pet's nose
- Keep the pet still using treats or similar
- Display the status delivered through
onDetectionStatusin the UI to guide the user
Q. The preview froze after switching screens
A. If you leave the screen without cleaning up the controller, the camera may not open in the next session. Call controller.release() when the screen is dismissed.
Next Steps
Once you've learned the basics, see the following documents:
- Fully Custom UI - Build a 100% custom UI with
CameraController+ your own SurfaceView - Customization - Customize the
CameraViewguide UI - Sound Guide - Configuring sound playback
- Fragment Approach (Legacy) - The existing
PetnowCameraFragmentintegration approach