Aptabase hashes its user_id from IP + user agent with a salt that rotates daily, so events can never be linked across days and no retention or MAU figure can come out of the export. In 19 days of data not one of 82 user_ids appears on two dates, which is the artefact, not the behaviour. Instead of an identity, every event now carries this install's own counters, kept in UserDefaults/DataStore: tenure_days, launch_no, active_days and dormant_days. Only derived day counts leave the device, so the privacy position is unchanged. They make the curve countable in the export: launch_no == 1 marks exactly one launch per install, dormant_days >= 1 exactly one launch per calendar day, so D_k is the share of installs seen again with tenure_days == k. Event date minus tenure_days is the install date, which gives full cohort tables. Installs predating the counters have no install date and report tenure_days == -1 forever, so they can be excluded instead of inflating the new-install cohort. The Kotlin counter arithmetic sits in a pure advance() so it can be tested without a Context; this adds the app module's first JVM test source set.
Cable for Android
A native Kotlin / Jetpack Compose port of the Cable by VoltPlan iOS app — a tool for sizing low-voltage electrical conductors (boats, RVs, off-grid). This module reproduces every feature of the iOS app with an Android-adapted navigation structure and the same Aptabase analytics backend.
Feature parity with iOS
- Systems — list, create (with name-derived icons), delete, onboarding empty state.
- System detail — Android bottom-navigation tabs (Overview · Components · Batteries · Chargers),
replacing the iOS
TabView. - Calculator / Loads — voltage/current/power/length sliders with snap-to-common-values and tap-to-edit dialogs, watt⇄ampere mode, duty cycle & daily on-time (advanced), live wire-gauge / voltage-drop / power-loss / fuse results, and a per-load Bill of Materials sheet.
- Batteries — chemistry picker, nominal voltage, capacity, usable-capacity override, charge & cut-off voltage, temperature range; bank voltage/capacity mismatch warnings.
- Chargers — power source picker (shore/solar/wind/generator/alternator), input/output voltage, and a charge-output field that toggles between current and power entry.
- Overview — estimated runtime & charge time with editable goals, BOM completion progress, and loads/batteries/chargers summary cards.
- Bill of Materials — categorized checklist (components, batteries, cables, fuses, accessories), completion tracking persisted per component, and locale-aware Amazon affiliate / search links.
- Component Library — VoltPlan PocketBase backend (
https://base.voltplan.app) with pagination, multi-language translations, locale-aware affiliate resolution, and Coil-cached remote icons. - PDF export — system overview and BOM PDFs via Android
PdfDocument, shared through aFileProvider. - Localization — English, German, Spanish, French, Dutch (
values,values-de/es/fr/nl). - Analytics — Aptabase (app key
A-SH-4260269603, hosthttps://apta.yuzuhub.com), implemented against the Aptabase ingestion protocol so every iOS event (App Launched,System Created,Tab Changed,Load/Battery/Charger Created/Deleted,BOM Item Tapped, …) is mirrored. The iOS app uses Aptabase, so Aptabase is the source of truth here.
Architecture
- UI: Jetpack Compose + Material 3, Navigation-Compose.
- State:
ViewModel+StateFlow; auto-save editors mirror the iOS "save on change" behaviour. - Persistence: Room (
SwiftData→@Entity), all values stored in metric; imperial is a display-time conversion only (UnitSystemSettingsvia DataStore). - Calculation engine:
calc/ElectricalCalculations.ktis a direct port (0.017 Ω·mm²/m copper resistivity, 5 % voltage-drop limit, AWG/metric tables). - Networking: Retrofit + kotlinx.serialization (PocketBase), OkHttp (Aptabase).
Package root: app.voltplan.cable.
Building
This module needs the Android toolchain (JDK 17 + Android SDK 35). The Gradle wrapper JAR is a binary and is not checked in here, so generate it once (or just open the folder in Android Studio, which does it automatically):
cd android
# Option A: open in Android Studio (Giraffe+) and let it sync.
# Option B: with a system Gradle 8.11+ installed:
gradle wrapper --gradle-version 8.11.1
./gradlew :app:assembleDebug
Create a local.properties pointing at your SDK if Studio doesn't:
sdk.dir=/path/to/Android/sdk
Notes
- SF Symbol names from the iOS data model are preserved verbatim and translated to Material icons at
render time (
ui/Symbols.kt), so the two platforms share identical stored data. - The in-calculator "saved loads" picker from iOS is intentionally omitted; the VoltPlan component library is the primary picker on Android.