# Calm Trace AR — build spec

A calmer AR tracing app that promises uninterrupted camera tracing, minimal permissions, stable overlays, and local photo-to-outline preparation instead of ad-heavy steps before drawing.

## Project context

This spec describes an independent, alternative Android app you are building from scratch to compete with an existing incumbent app on Google Play — not a modification, clone, or reskin of the incumbent's own code, assets, or branding. Use the incumbent only as a market reference (via the report data below), and design working_name/package_id/store_listing/design_system so the result is clearly its own product.

## Incumbent app

- **Name:** AR Drawing - Paint & Sketch
- **Package id:** `ar.drawing.anime.paint.sketching.tracing.draw.apps`
- **Google Play:** https://play.google.com/store/apps/details?id=ar.drawing.anime.paint.sketching.tracing.draw.apps
- **appy.fyi report:** https://appy.fyi/report/ar.drawing.anime.paint.sketching.tracing.draw.apps
- **Category:** Art & Design

## Overview

- **Working name:** Calm Trace AR (trademark cleared: no)
- **Package id:** `fyi.appy.calmtracear`
- **Min / target SDK:** 26 / 35
- **Backend:** none
- **Estimated build time:** 8 weeks
- **Pricing:** subscription, $2.99 via `revenuecat`
- **Runtime AI:** none
- **Permissions:** `CAMERA`, `INTERNET`

## Non-goals (out of scope for v1)

- No interstitial ads, banner ads, browser redirects, store redirects, or third-party ad SDK in v1.
- No community feed, public sharing network, creator marketplace, comments, likes, or user accounts.
- No generative AI art, AI image creation, or claims that the app magically draws for the user.
- No audio recording, microphone permission, broad external-storage permission, contact access, or tracking prompts.
- No advanced full drawing-suite tools such as brushes, layers, color painting, animation, or games.
- No cloud sync across devices in v1; saved projects remain local to the device.
- Do not target the app specifically at children; use safe templates and privacy-light wording, but avoid child-directed marketing.

## Tech stack

- **Language / UI:** Kotlin, Jetpack Compose
- **Kotlin:** 2.0.21
- **Compose BOM:** 2024.10.01
- **Gradle:** 8.9

| Purpose | Gradle coordinate |
| --- | --- |
| Android Kotlin extensions and compatibility helpers | `androidx.core:core-ktx:1.15.0` |
| Compose activity integration and Activity Result APIs for camera permission and photo picker | `androidx.activity:activity-compose:1.9.3` |
| Material 3 Compose components for screens, sliders, buttons, cards, sheets, and dialogs | `androidx.compose.material3:material3:1.3.0` |
| Compose Navigation graph for onboarding, home, import, tracing, saved projects, export, paywall, and settings routes | `androidx.navigation:navigation-compose:2.8.4` |
| Lifecycle-aware Compose state collection | `androidx.lifecycle:lifecycle-runtime-compose:2.8.7` |
| CameraX core camera APIs | `androidx.camera:camera-core:1.4.0` |
| Camera2-backed CameraX implementation | `androidx.camera:camera-camera2:1.4.0` |
| Bind CameraX preview lifecycle to Compose screens | `androidx.camera:camera-lifecycle:1.4.0` |
| CameraX PreviewView for the AR tracing camera surface | `androidx.camera:camera-view:1.4.0` |
| Room runtime for saved tracing projects | `androidx.room:room-runtime:2.6.1` |
| Room Kotlin coroutines extensions for DAO flows and suspend queries | `androidx.room:room-ktx:2.6.1` |
| Room annotation processor for entities and DAOs | `androidx.room:room-compiler:2.6.1` |
| Coil Compose image loading for imported photos, generated outline PNGs, templates, and project thumbnails | `io.coil-kt:coil-compose:2.7.0` |
| Preferences DataStore for onboarding-complete flag, selected outline strength, and non-sensitive settings | `androidx.datastore:datastore-preferences:1.1.1` |
| RevenueCat subscription entitlement handling for the monthly Pro plan | `com.revenuecat.purchases:purchases:8.11.0` |
| Kotlin coroutines on Android for Room, image processing, and billing state flows | `org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0` |
| Local JVM unit tests for image-processing thresholds, project limits, and entitlement logic | `junit:junit:4.13.2` |
| Android instrumented JUnit runner assertions | `androidx.test.ext:junit:1.2.1` |
| Espresso base dependency for instrumented UI interactions | `androidx.test.espresso:espresso-core:3.6.1` |
| Compose UI test APIs for navigation, paywall, and no-interruption session tests | `androidx.compose.ui:ui-test-junit4:1.7.5` |

## Design system

- **Primary color:** `#176B5B`
- **Background color:** `#FFFDF7`
- **Error color:** `#B3261E`
- **Typography:** Material 3 default type scale using system font; no custom downloadable font.
- **Launcher icon glyph:** Phosphor `camera` (regular weight)
- **Theme notes:** Use Material 3 light and dark color schemes derived from the primary teal. Light background is #FFFDF7; dark background is #121614. Camera controls use semi-transparent black scrims (#000000 at 54% alpha) over the preview so imported outlines remain visible. Do not use flashing animations or full-screen promotional modals inside the tracing session.

## Screens

### OnboardingAndPermissions
- **Route:** `onboarding`
- **Purpose:** Explain the no-interruption tracing promise, why camera access is needed, and that gallery import uses the Android photo picker without broad file access.
- **Reached via:** app launch when onboardingComplete is false; tap Privacy & permissions in Settings
- **Key UI elements:** App logo and title; Three promise cards: No ads in tracing, Camera only when tracing, Photos stay on device; Camera permission explanation panel; Continue button; Skip import explanation link; Privacy policy link
- **States:** initial, camera_permission_not_requested, camera_permission_granted, camera_permission_denied, camera_permission_permanently_denied

### Home
- **Route:** `home`
- **Purpose:** Main entry point for starting from camera, importing a photo, opening safe templates, or continuing saved projects.
- **Reached via:** app launch after onboarding; system back from ImportEditor; system back from SavedProjects; successful purchase dismissal from Paywall
- **Key UI elements:** Start camera tracing button; Import photo button; Template browser preview row; Saved projects preview row; Pro status chip; Settings icon
- **States:** loading, empty_no_projects, populated, error_loading_projects

### ImportEditor
- **Route:** `import/{sourceType}/{sourceId}`
- **Purpose:** Prepare an imported photo or template as a traceable overlay with local outline generation, opacity preview, and stencil strength selection before entering camera mode.
- **Reached via:** tap Import photo on Home after Android photo picker returns a Uri; tap a template tile on Home; tap a template tile in TemplateBrowser; tap edit on a saved project
- **Key UI elements:** Original image preview; Outline preview toggle; Stencil strength segmented control: Soft, Medium, Bold; Opacity slider; Crop-to-fit toggle; Use in camera button; Save as project button; Error message with retry
- **States:** loading_image, processing_outline, ready_original, ready_outline, unsupported_image_error, processing_error, save_success

### TemplateBrowser
- **Route:** `templates`
- **Purpose:** Browse a small safe local template set and choose free or Pro templates without disruptive ads or redirects.
- **Reached via:** tap Template browser preview row on Home; tap See all templates from ImportEditor
- **Key UI elements:** Category chips; Template grid; Free/Premium badges; Search field for local template names; Open selected template button; Pro unlock prompt for premium template taps
- **States:** loading, empty, populated, premium_locked, error_loading_assets

### TraceSession
- **Route:** `trace/{projectId}`
- **Purpose:** Camera-based AR tracing session with a stable overlay, resize, opacity, lock, flashlight, and focus controls, with no paywalls or prompts while drawing.
- **Reached via:** tap Use in camera from ImportEditor; tap Continue on a saved project; tap Start camera tracing on Home after choosing a recent project
- **Key UI elements:** Camera preview; Trace overlay image; Opacity slider; Lock/unlock overlay button; Flashlight toggle; Reset position button; Save button; Export button; Back confirmation sheet; Focus reticle on tap
- **States:** requesting_camera_permission, camera_starting, camera_ready_unlocked, camera_ready_locked, camera_permission_denied, camera_unavailable_error, torch_unavailable, saving, save_error

### SavedProjects
- **Route:** `projects`
- **Purpose:** Show locally saved tracing projects and enforce the free-tier saved-project limit without deleting user work unexpectedly.
- **Reached via:** tap Saved projects preview row on Home; tap Projects from Settings; after saving a project from TraceSession
- **Key UI elements:** Project list/grid; Project thumbnail; Created/updated timestamp; Open button; Duplicate button; Delete with confirmation; Free project limit banner; Upgrade button
- **States:** loading, empty, populated, limit_reached, delete_confirming, error_loading_projects

### ExportPreview
- **Route:** `export/{projectId}`
- **Purpose:** Preview and share an exported tracing reference image, applying watermark rules based on Pro entitlement.
- **Reached via:** tap Export in TraceSession; tap Export from a saved project overflow menu
- **Key UI elements:** Export preview image; Watermark notice for free tier; Share button; Save to Photos button; Upgrade for watermark-free button; Export success snackbar
- **States:** loading_project, rendering_export, ready_free_watermarked, ready_pro_watermark_free, save_success, share_sheet_opened, export_error

### Paywall
- **Route:** `paywall/{source}`
- **Purpose:** Offer the $2.99/month Pro plan for premium templates, watermark-free export, and more saved projects without blocking the active drawing session.
- **Reached via:** tap Pro status chip on Home; tap premium template while not Pro; tap Upgrade for watermark-free in ExportPreview; tap Upgrade from SavedProjects limit banner; tap Manage Pro in Settings
- **Key UI elements:** Pro benefits list; Monthly price $2.99; Subscribe button; Restore purchases button; Not now button; Terms and privacy links
- **States:** loading_offerings, offer_available, purchase_pending, purchase_success, purchase_cancelled, restore_success, billing_error, offline_error

### Settings
- **Route:** `settings`
- **Purpose:** Expose privacy, permission, billing, and support information in one predictable place.
- **Reached via:** tap Settings icon on Home; tap settings link from OnboardingAndPermissions
- **Key UI elements:** No disruptive ads policy text; Camera permission status; Photo picker explanation; Manage Pro button; Restore purchases button; Privacy policy link; App version
- **States:** loading_entitlement, populated_free, populated_pro, restore_pending, restore_success, restore_error

## Data model

### ProjectEntity (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `Long` | primary key, autogenerate |
| title | `String` | user-visible project title; default is Template name or Imported photo timestamp |
| sourceImagePath | `String` | absolute path inside app-specific internal files directory for the copied original image |
| outlineImagePath | `String` | nullable absolute path inside app-specific internal files directory for generated transparent PNG outline |
| templateAssetId | `String` | nullable; set when project was created from a bundled template asset |
| createdAtEpochMillis | `Long` | UTC epoch milliseconds |
| updatedAtEpochMillis | `Long` | UTC epoch milliseconds |
| opacity | `Float` | 0.0 to 1.0 overlay opacity; default 0.65 |
| scale | `Float` | overlay scale multiplier; default 1.0 |
| offsetX | `Float` | overlay horizontal offset in preview pixels |
| offsetY | `Float` | overlay vertical offset in preview pixels |
| rotationDegrees | `Float` | overlay rotation in degrees; default 0.0 |
| locked | `Boolean` | true disables transform gestures in TraceSession |
| lastStencilStrength | `String` | one of SOFT, MEDIUM, BOLD; default MEDIUM |

### ProjectExportEntity (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `Long` | primary key, autogenerate |
| projectId | `Long` | foreign key to ProjectEntity.id; cascade delete |
| exportPath | `String` | absolute path inside app-specific cache or files directory for last rendered export PNG |
| watermarked | `Boolean` | true when rendered under free entitlement |
| createdAtEpochMillis | `Long` | UTC epoch milliseconds |

## Features

### Minimal onboarding and permission copy

Explain the calm tracing flow and request only camera access when the user is ready to trace.

- **Answers complaint:** Permissions and child safety

- **Screens:** OnboardingAndPermissions, Home, TraceSession, Settings

- **Estimated hours:** 24

**Implementation notes:** On first launch, navigate to OnboardingAndPermissions and store onboardingComplete=true in Preferences DataStore only after the user taps Continue. Do not request CAMERA on app launch. Request CAMERA with ActivityResultContracts.RequestPermission only when the user taps Start camera tracing or Use in camera. For gallery import, use ActivityResultContracts.PickVisualMedia with PickVisualMedia.ImageOnly so no READ_MEDIA_IMAGES permission is requested. If CAMERA is denied once, show an inline explanation and a Retry button. If shouldShowRequestPermissionRationale is false after denial, show an Open Settings button using Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + packageName)). Never request RECORD_AUDIO or MANAGE_EXTERNAL_STORAGE anywhere in the manifest or runtime code.

**Acceptance criteria:**
- Fresh install shows onboarding before Home and does not trigger an Android permission dialog automatically.
- Tapping Import photo opens the system photo picker and the app manifest does not contain READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, MANAGE_EXTERNAL_STORAGE, or RECORD_AUDIO.
- Tapping Use in camera triggers exactly one CAMERA permission request if permission has not already been granted.
- After permanent camera denial, the screen shows an Open Settings action instead of repeatedly showing the permission dialog.

### No-interruption create-mode policy

Keep all selecting, importing, resizing, locking, and tracing flows free of interstitials, banners, redirects, and surprise paywalls.

- **Answers complaint:** Ads block the core flow

- **Screens:** Home, ImportEditor, TemplateBrowser, TraceSession, SavedProjects, ExportPreview, Paywall

- **Estimated hours:** 32

**Implementation notes:** Do not include any advertising SDK dependency or ad view. Gate Pro-only actions with explicit user-initiated navigation to Paywall only from premium template taps, SavedProjects limit banner, ExportPreview watermark-free button, Home Pro chip, or Settings Manage Pro. Add a NavigationGuard object with a single function canShowPaywall(currentRoute, trigger) that returns false for route trace/{projectId} regardless of trigger. TraceSession must not call Paywall navigation, browser Intents, Play Store Intents, or billing purchase methods. Enforce this with a unit test over the route/trigger matrix and an instrumented test that starts TraceSession, uses opacity/lock/torch/reset controls, and verifies the current route remains trace/{projectId}.

**Acceptance criteria:**
- No Gradle dependency group starting with com.google.android.gms:play-services-ads or other ad SDK is present.
- During TraceSession, tapping opacity, lock, unlock, flashlight, reset, save, and back does not navigate to Paywall or open an external Intent.
- Premium template taps outside TraceSession show Paywall with a Not now button and return the user to TemplateBrowser when dismissed.
- The app never launches ACTION_VIEW for browser, store, Shopee/TikTok-style destinations, or any external URL except explicit privacy/terms links tapped by the user.

### Safe local template browser

Provide a small bundled template set with free and Pro items that can be opened instantly without network loading or inappropriate ad content.

- **Answers complaint:** Permissions and child safety

- **Screens:** Home, TemplateBrowser, ImportEditor, Paywall

- **Estimated hours:** 36

**Implementation notes:** Store template metadata in assets/templates/templates.json with fields id, title, category, assetPath, tier where tier is FREE or PRO. Store transparent PNG template outlines under assets/templates/free/ and assets/templates/pro/. At startup, load the JSON from assets on Dispatchers.IO, validate that every assetPath opens successfully, and render the grid with Coil using file:///android_asset paths. Use only safe categories such as Animals, Plants, Simple objects, and Cute shapes. Do not include brands, suggestive content, weapons, gambling, or user-generated templates. If a PRO template is tapped and RevenueCat entitlement pro is inactive, navigate to Paywall with source=premium_template; otherwise create a ProjectEntity from the asset and navigate to ImportEditor.

**Acceptance criteria:**
- TemplateBrowser works with airplane mode enabled because all template metadata and PNGs are bundled assets.
- Tapping a FREE template creates a project and opens ImportEditor without showing Paywall.
- Tapping a PRO template while entitlement is inactive opens Paywall; tapping Not now returns to TemplateBrowser with no project created.
- All template tiles display a visible FREE or PRO badge and a category chip filter changes the visible set without a network request.

### Local photo-to-outline import

Turn an imported photo into a clean traceable outline locally, with Soft, Medium, and Bold stencil levels.

- **Answers complaint:** Lag, freezing, and camera trust

- **Screens:** Home, ImportEditor

- **Estimated hours:** 56

**Implementation notes:** Use ActivityResultContracts.PickVisualMedia for image selection. Immediately copy the returned Uri stream to context.filesDir/imports/{uuid}.jpg so the project does not depend on long-term external Uri access. Decode with ImageDecoder on API 28+ and BitmapFactory on API 26-27, downsampling so max(width,height) <= 1600. Generate outline PNG on Dispatchers.Default: convert each pixel to luminance using 0.299*r + 0.587*g + 0.114*b; apply a fixed 3x3 Gaussian blur kernel [1,2,1;2,4,2;1,2,1]/16; compute Sobel gx and gy; magnitude=sqrt(gx*gx+gy*gy). For SOFT threshold at magnitude >= 48, MEDIUM >= 36, BOLD >= 24. Output transparent pixels for non-edges and black pixels with alpha 230 for edges. Save as filesDir/outlines/{uuid}_{strength}.png. Cancel and replace the processing job when the user changes stencil strength quickly. Show processing_outline state until the PNG exists.

**Acceptance criteria:**
- Importing a valid JPEG or PNG creates a copied source file under app internal files and never stores the original external Uri as the only source.
- Changing stencil strength from Soft to Bold changes the threshold and regenerates the preview PNG without crashing.
- A 4000x3000 image is downsampled before processing so the decoded bitmap max dimension is 1600 or less.
- Unsupported or corrupt image input shows unsupported_image_error with a Retry import button and does not create a ProjectEntity.

### Stable CameraX tracing overlay

Show the live camera preview with a movable, resizable, lockable, opacity-controlled trace overlay and flashlight support.

- **Answers complaint:** Lag, freezing, and camera trust

- **Screens:** TraceSession

- **Estimated hours:** 84

**Implementation notes:** Implement TraceSession with AndroidView wrapping CameraX PreviewView using ImplementationMode.PERFORMANCE. Obtain ProcessCameraProvider, bind Preview only to the lifecycle with CameraSelector.DEFAULT_BACK_CAMERA, and do not bind ImageAnalysis for v1. Overlay the selected outline/source image as a Compose Image above the PreviewView. Maintain OverlayTransform state from ProjectEntity: scale, offsetX, offsetY, rotationDegrees, opacity, locked. Use Modifier.graphicsLayer for alpha, scale, translation, and rotation. Use detectTransformGestures to update pan/zoom/rotation only when locked=false, clamping scale to 0.25..6.0 and opacity slider to 0.10..1.0. Save transform changes to Room when Save is tapped and on lifecycle ON_STOP. Implement torch by checking camera.cameraInfo.hasFlashUnit() and calling camera.cameraControl.enableTorch(boolean); show torch_unavailable if false. Implement tap-to-focus by converting tap coordinates with PreviewView.meteringPointFactory and calling camera.cameraControl.startFocusAndMetering(FocusMeteringAction.Builder(point).setAutoCancelDuration(3, TimeUnit.SECONDS).build()). Add FLAG_KEEP_SCREEN_ON while TraceSession is visible and clear it on dispose.

**Acceptance criteria:**
- Camera preview starts with only the back camera Preview use case bound; no ImageAnalysis use case is bound.
- Dragging and pinching move and resize the overlay while unlocked, and the same gestures do nothing while locked.
- Opacity slider visibly changes overlay alpha between 10% and 100% without restarting the camera.
- Flashlight button enables torch on devices with flash and shows a non-crashing unavailable message on devices without flash.
- Leaving and reopening a project restores the last saved scale, offset, rotation, opacity, and locked state.

### Saved projects, export, and share

Let users save tracing setups locally, continue them later, and export/share a watermarked or watermark-free reference image based on entitlement.

- **Answers complaint:** baseline parity

- **Screens:** SavedProjects, TraceSession, ExportPreview, Paywall

- **Estimated hours:** 40

**Implementation notes:** Persist each tracing setup as ProjectEntity in Room. Free tier allows up to 5 active projects; Pro entitlement removes this limit. If a free user reaches 5 projects, allow opening, editing, exporting, and deleting existing projects, but block creating a sixth with a SavedProjects limit banner and Upgrade button. For export, render a 1600x1600 ARGB_8888 bitmap with the source or outline centered using the project transform normalized to the export canvas. If entitlement pro is inactive, draw a bottom-right watermark text "Calm Trace AR" using Paint color #66000000 at 32sp-equivalent pixels and set watermarked=true in ProjectExportEntity. Save exports to MediaStore.Images on API 29+ with RELATIVE_PATH Pictures/Calm Trace AR and IS_PENDING during write; on API 26-28, write to getExternalFilesDir(Environment.DIRECTORY_PICTURES) and share from FileProvider. Use ACTION_SEND with image/png MIME for sharing.

**Acceptance criteria:**
- A saved project appears in SavedProjects after tapping Save in TraceSession.
- A free user with 5 projects can open and delete existing projects but cannot create a sixth until deleting one or subscribing.
- Free export preview contains the visible text Calm Trace AR in the bottom-right; Pro export does not contain that watermark.
- Tapping Share opens the Android share sheet with MIME type image/png and a content Uri, not a file:// Uri.
- Export failure leaves the original project intact and shows export_error.

### Monthly Pro subscription

Offer the report-specified $2.99/month Pro plan for premium templates, watermark-free export, and more saved projects.

- **Answers complaint:** baseline parity

- **Screens:** Home, TemplateBrowser, SavedProjects, ExportPreview, Paywall, Settings

- **Estimated hours:** 32

**Implementation notes:** Use RevenueCat with a monthly product identifier pro_monthly_299 and entitlement identifier pro. Configure Purchases in Application.onCreate with the public RevenueCat API key supplied by build config field REVENUECAT_API_KEY. On Paywall, call Purchases.sharedInstance.getOfferings(); select current monthly package whose storeProduct.id == "pro_monthly_299"; display its localized price, but fallback text must still say $2.99/month if offerings fail after timeout. Purchase with purchasePackage and update a StateFlow<EntitlementState> from CustomerInfo.entitlements["pro"].isActive. Restore with restorePurchases. Cache only the last known entitlement boolean and timestamp in DataStore for UI display; source of truth remains RevenueCat CustomerInfo. Never start purchases automatically; only Subscribe button starts purchase.

**Acceptance criteria:**
- Paywall displays a monthly Pro offer and a Subscribe button only after offerings load successfully.
- Purchase success sets entitlement pro active and immediately unlocks premium templates, removes export watermark, and removes the 5-project limit.
- Restore purchases updates the same entitlement state and shows restore_success when pro is active.
- Cancelling purchase leaves entitlement inactive and returns to the previous screen without deleting any project or template choice.

### Device stability hardening for camera sessions

Reduce camera lag, heating, and rotation issues by keeping the AR session lightweight and lifecycle-safe.

- **Answers complaint:** Lag, freezing, and camera trust

- **Screens:** TraceSession, ImportEditor

- **Estimated hours:** 16

**Implementation notes:** Keep TraceSession camera workload to Preview only; pause all outline processing jobs before navigating into TraceSession. Bind camera in DisposableEffect keyed by lifecycleOwner and unbindAll in onDispose. Use PreviewView scaleType FILL_CENTER and Compose BoxWithConstraints to recompute overlay bounds when rotation or window size changes. Store transform values in preview-coordinate-independent normalized form on save: normalizedOffsetX = offsetX / previewWidth and normalizedOffsetY = offsetY / previewHeight; convert back when the preview size changes. Add a 500 ms debounce to Room writes for transform autosave so continuous gestures do not write on every frame. If camera binding throws, show camera_unavailable_error with Retry and Back buttons rather than a blank screen.

**Acceptance criteria:**
- Entering TraceSession cancels any active photo outline processing coroutine from ImportEditor.
- Rotating the device keeps the overlay visible within the preview bounds and does not reset opacity or lock state.
- Camera provider unbindAll is called when leaving TraceSession, verified by an instrumented fake lifecycle test or logged test hook.
- Continuous pinch gesture for 5 seconds results in debounced Room writes, not one write per frame.
- If camera binding fails, the user sees camera_unavailable_error with Retry and Back actions.

## Store listing

- **Title:** Calm Trace AR
- **Short description:** Ad-light AR tracing with photo outlines, templates, and a stable camera overlay.
- **Category:** Art & Design
- **Keywords:** AR tracing, drawing, trace photo, camera drawing, stencil, line art, sketching, template tracing
- **Icon prompt:** Create a square Android app icon for an AR tracing utility named Calm Trace AR. Use a calm deep teal background (#176B5B) with a simple white line-art camera glyph and a subtle pencil outline crossing the lower-right corner. Minimal flat vector style, rounded corners, no text, no gradients, no ads, no mascot, high contrast, suitable for Play Store launcher icon.

**Long description:**

Trace drawings without fighting interruptions. Calm Trace AR helps you import a photo or choose a safe local template, turn it into a clean outline, and place it over your live camera view for easy paper tracing.

Built for a focused tracing loop:
• No ads or popups during selecting, resizing, locking, or tracing
• Camera overlay with opacity, resize, rotate, lock, and flashlight controls
• Import photos through Android’s photo picker
• Local outline generation with Soft, Medium, and Bold stencil levels
• Small safe template set included on device
• Save projects and continue later
• Export and share tracing references

Privacy-light by design: the app asks for camera access only for tracing and does not request microphone or broad storage permissions.

Pro is available for $2.99/month and unlocks premium templates, watermark-free export, and more saved projects. The free tier remains usable so you can complete real drawings before deciding to upgrade.

## Legal

- **Regulated category:** none
- **Privacy policy URL:** https://example.com/calm-trace-ar/privacy (privacy claims verified: no)
- **Data collected:** Purchase history for Pro subscription entitlement; Anonymous app instance identifier used by RevenueCat for subscription status

## Test plan

### 1. Ads block the core flow (instrumented)

1. Install a fresh debug build.
2. Launch the app and complete OnboardingAndPermissions without granting camera yet.
3. From Home tap Import photo and select a test PNG through the system photo picker.
4. Wait for ImportEditor state ready_outline.
5. Tap Use in camera, grant CAMERA, and wait for TraceSession state camera_ready_unlocked.
6. Tap opacity slider, lock, unlock, reset position, flashlight, save, and back confirmation cancel.
7. Record every navigation route emitted by the NavController during these interactions.

**Expected:** The route remains within ImportEditor and TraceSession for the create flow; Paywall is never shown, no external ACTION_VIEW intent is fired, and no ad view exists in the hierarchy.

### 2. Redirects and forced openings (unit)

1. Scan the resolved debug runtime classpath dependencies.
2. Assert no dependency coordinate contains play-services-ads, applovin, unity-ads, facebook.ads, ironsource, or vungle.
3. Run NavigationGuard.canShowPaywall for currentRoute trace/{projectId} with triggers premium_template, export_watermark_free, project_limit, home_pro_chip, and settings_manage_pro.
4. Run an intent allowlist test over app code paths that create ACTION_VIEW intents.

**Expected:** No ad SDK dependency is present; NavigationGuard returns false for every TraceSession trigger; ACTION_VIEW is only allowed for explicit privacy policy or terms links tapped from Settings or Paywall.

### 3. Permissions and child safety (instrumented)

1. Install a fresh build and inspect requested permissions with packageManager.getPackageInfo(packageName, GET_PERMISSIONS).
2. Launch the app and stay on OnboardingAndPermissions for 3 seconds.
3. Tap Import photo.
4. Cancel the system photo picker.
5. Tap Start camera tracing from Home or Use in camera after selecting a sample image.

**Expected:** The manifest requests CAMERA and INTERNET only; no permission dialog appears on launch or photo import; the CAMERA permission dialog appears only when entering camera tracing.

### 4. Lag, freezing, and camera trust (unit)

1. Load a 4000x3000 JPEG test fixture from test resources.
2. Run the import downsample function.
3. Run outline generation for SOFT, MEDIUM, and BOLD strengths.
4. Measure output bitmap dimensions and count non-transparent edge pixels for each strength.

**Expected:** The downsampled bitmap max dimension is 1600 or less; each outline PNG is produced without OutOfMemoryError; BOLD has more or equal edge pixels than MEDIUM, and MEDIUM has more or equal edge pixels than SOFT.

### 5. Lag, freezing, and camera trust (manual)

1. Use a mid-range Android device with rear camera and flashlight.
2. Import a sample outline and enter TraceSession.
3. Keep TraceSession open for 10 minutes with screen awake.
4. During the session, pinch/drag the overlay for 30 seconds, toggle lock twice, toggle flashlight twice, rotate the device twice, and tap to focus on a high-contrast object.
5. Exit to Home and reopen the same project.

**Expected:** The camera preview remains live, overlay controls respond within one second, the app does not crash or show a blank camera screen, rotation keeps the overlay visible, and reopening restores the last saved transform.

### 6. baseline parity (instrumented)

1. Create five ProjectEntity rows as a free user in the test database.
2. Open SavedProjects and verify the limit banner is visible.
3. Attempt to create a sixth project from a free template.
4. Delete one existing project.
5. Create a new project from the same free template.

**Expected:** The sixth project is blocked while five projects exist; deleting one project removes the block; creating a new project succeeds after the count drops below five.

### 7. baseline parity (instrumented)

1. Create one project with an outline image in Room.
2. Set entitlement state to free and open ExportPreview.
3. Tap Save to Photos and capture the rendered bitmap from the export renderer test hook.
4. Set entitlement state to pro and render the same export again.

**Expected:** The free rendered bitmap includes the bottom-right Calm Trace AR watermark and records watermarked=true; the Pro rendered bitmap does not include the watermark and records watermarked=false.

### 8. baseline parity (instrumented)

1. Mock RevenueCat offerings with product id pro_monthly_299 and localized price $2.99.
2. Open Paywall from the Home Pro chip.
3. Tap Subscribe and return mocked purchase success with entitlement pro active.
4. Navigate to TemplateBrowser and tap a premium template.
5. Navigate to ExportPreview for a project.

**Expected:** Paywall shows $2.99/month; after purchase, premium template opens ImportEditor without Paywall and ExportPreview enters ready_pro_watermark_free state.

## Build instructions

```sh
./gradlew clean
./gradlew testDebugUnitTest
./gradlew connectedDebugAndroidTest
keytool -genkeypair -v -keystore calm-trace-upload.jks -storepass changeit-change-before-upload -alias calm-trace-upload -keypass changeit-change-before-upload -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Calm Trace AR Upload,O=Solo Builder,C=US"
ANDROID_KEYSTORE_PATH=$PWD/calm-trace-upload.jks ANDROID_KEYSTORE_PASSWORD=changeit-change-before-upload ANDROID_KEY_ALIAS=calm-trace-upload ANDROID_KEY_PASSWORD=changeit-change-before-upload REVENUECAT_API_KEY=replace_with_revenuecat_public_sdk_key ./gradlew bundleRelease
```

## Human gates still required

- `trademark_and_privacy_review`
- `closed_testing_recruitment`
