Petnow LogoPetnow

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

ElementDescription
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 separateinitializeCamera() 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 a Result.
  • pauseDetection() — Pauses detection only. Keeps the preview/progress. Returns nothing.
  • resumeDetection() — Resumes from where it was paused. Returns a Result.

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_KEY

Do 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:

  1. Whether the camera permission (CAMERA) is declared in AndroidManifest.xml
  2. Whether you requested the runtime permission before starting the session (CameraView does not request it itself)
  3. Whether you connected the controller with cameraView.controller = controller
  4. Whether you called startDetection() after initializeCamera()

Q. Capture keeps failing

A. Review the capture environment:

  1. Capture under bright indoor lighting (avoid direct sunlight and shadows)
  2. Keep a distance of 30–50 cm between the camera and the pet's nose
  3. Keep the pet still using treats or similar
  4. Display the status delivered through onDetectionStatus in 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:

On this page