Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

VelocityTimer — Native Android Speedcubing Timer

A pixel-perfect Android conversion of the CubeFlow web app.
Built with Kotlin + Jetpack Compose, targeting AMOLED displays.


Stack

Concern Solution
UI Jetpack Compose (Canvas, Modifier, Animation)
Timer precision SystemClock.elapsedRealtimeNanos() — sub-ms accuracy
Multi-touch awaitPointerEventScope — tracks all PointerIds simultaneously
Database Room (SQLite) — solves, profiles tables
Settings DataStore Preferences
State management ViewModel + StateFlow + collectAsStateWithLifecycle
Fonts Google Fonts (Orbitron, Share Tech Mono) — downloaded at runtime
Screen wake PowerManager.WakeLock + FLAG_KEEP_SCREEN_ON
Haptics VibrationEffect.createOneShot() — API 26+

Project Structure

VelocityTimer/
├── app/src/main/
│   ├── AndroidManifest.xml
│   ├── java/com/velocitytimer/app/
│   │   ├── VelocityTimerApp.kt          ← Application class / DI root
│   │   ├── MainActivity.kt              ← Single activity, edge-to-edge
│   │   ├── navigation/
│   │   │   └── NavGraph.kt              ← Compose Navigation host
│   │   ├── ui/
│   │   │   ├── theme/
│   │   │   │   ├── Color.kt             ← AMOLED palette + LED tokens
│   │   │   │   ├── Theme.kt             ← MaterialTheme wrapper
│   │   │   │   └── Type.kt              ← Orbitron + Share Tech Mono
│   │   │   ├── components/
│   │   │   │   ├── GlassTouchPad.kt     ← Full-height multi-touch pad
│   │   │   │   ├── LedTimer.kt          ← Canvas 7-segment display
│   │   │   │   ├── StatusLed.kt         ← Animated LED dot indicator
│   │   │   │   ├── StatCard.kt          ← ao5/ao12/... glass card
│   │   │   │   └── GlassCard.kt         ← Reusable frosted surface
│   │   │   └── screens/
│   │   │       ├── TimerScreen.kt        ← Main timer + touch pads
│   │   │       ├── ScrambleScreen.kt     ← Scramble + all averages
│   │   │       ├── SettingsScreen.kt     ← Settings toggles + slider
│   │   │       ├── ProfileScreen.kt      ← Profile hero + PB + stats
│   │   │       └── CreateProfileScreen.kt← New profile form
│   │   ├── data/
│   │   │   ├── model/
│   │   │   │   ├── Solve.kt             ← Room entity
│   │   │   │   ├── Profile.kt           ← Room entity
│   │   │   │   └── AppSettings.kt       ← DataStore model
│   │   │   ├── db/
│   │   │   │   ├── AppDatabase.kt       ← Room database singleton
│   │   │   │   ├── SolveDao.kt          ← Solve CRUD + Flow
│   │   │   │   └── ProfileDao.kt        ← Profile CRUD + Flow
│   │   │   └── repository/
│   │   │       ├── SolveRepository.kt
│   │   │       ├── ProfileRepository.kt
│   │   │       └── SettingsRepository.kt
│   │   ├── utils/
│   │   │   ├── ScrambleGenerator.kt     ← WCA 3×3 scramble generator
│   │   │   ├── TimeFormatter.kt         ← ms → "9.87" / "1:02.34"
│   │   │   ├── AverageCalculator.kt     ← ao5/ao12/ao25/ao50/ao100
│   │   │   ├── WakeLockManager.kt       ← Replaces browser Wake Lock API
│   │   │   └── HapticHelper.kt          ← Replaces navigator.vibrate()
│   │   └── viewmodel/
│   │       ├── TimerViewModel.kt         ← State machine + timer loop
│   │       ├── ScrambleViewModel.kt      ← Reactive stats
│   │       ├── SettingsViewModel.kt      ← DataStore ↔ UI bridge
│   │       └── ProfileViewModel.kt       ← Profile CRUD
│   └── res/
│       ├── values/
│       │   ├── strings.xml
│       │   ├── colors.xml
│       │   ├── themes.xml               ← Fullscreen AMOLED black
│       │   └── font_certs.xml           ← Google Fonts provider certs
│       ├── xml/
│       │   ├── backup_rules.xml
│       │   └── data_extraction_rules.xml
│       ├── drawable/
│       │   ├── ic_launcher_background.xml
│       │   └── ic_launcher_foreground.xml
│       └── mipmap-anydpi-v26/
│           ├── ic_launcher.xml
│           └── ic_launcher_round.xml

Requirements

Tool Minimum version
Android Studio Hedgehog (2023.1.1) or newer
JDK 17
Android Gradle Plugin 8.3.2
Kotlin 1.9.24
minSdk 26 (Android 8.0)
targetSdk 34 (Android 14)

Opening in Android Studio

  1. Unzip VelocityTimer.zip
  2. Open Android Studio → File → Open
  3. Select the VelocityTimer/ root folder (the one containing settings.gradle.kts)
  4. Click OK — Android Studio will auto-detect the Gradle project
  5. Wait for the initial Gradle sync (~2–4 minutes on first run while dependencies download)
  6. When the sync completes with no errors, proceed to build

If Gradle sync fails with "Gradle wrapper not found": Run gradle wrapper --gradle-version=8.6 in the project root, or copy gradle/wrapper/gradle-wrapper.jar from any other Android Studio project.


Building

Debug APK (for testing / sideloading)

cd VelocityTimer
./gradlew assembleDebug

Output: app/build/outputs/apk/debug/app-debug.apk

Install directly to connected device

./gradlew installDebug
# or
adb install -r app/build/outputs/apk/debug/app-debug.apk

Release AAB (for Google Play)

./gradlew bundleRelease

Output: app/build/outputs/bundle/release/app-release.aab

You will need a signing keystore for release builds. In Android Studio: Build → Generate Signed Bundle/APK → Create new keystore


Google Fonts Setup (Optional)

The app uses Orbitron (display) and Share Tech Mono (timer/mono) from Google Fonts, downloaded at runtime via the Google Mobile Services fonts provider.

This works automatically on all devices with Google Play Services (virtually all Android phones).

If you need to support devices without GMS (e.g. Amazon Fire, custom ROMs):

  1. Download the fonts:

  2. Place them in app/src/main/res/font/

  3. Replace the FontFamily declarations in Type.kt:

val OrbitronFamily = FontFamily(
    Font(R.font.orbitron_regular,      FontWeight.Normal),
    Font(R.font.orbitron_bold,         FontWeight.Bold),
)
val ShareTechMonoFamily = FontFamily(
    Font(R.font.share_tech_mono_regular)
)
  1. Update DisplayFont and MonoFont aliases to point to the new families.

Timer Logic

The timer is a state machine with 6 states:

IDLE → ONE_HAND → HOLDING → READY → RUNNING → STOPPED → IDLE
State Condition LED colour
IDLE No pads held Dim red
ONE_HAND One pad held Amber (blinking)
HOLDING Both held, counting down Red (blinking)
READY Hold delay elapsed Green (solid)
RUNNING Released both from READY Blue (dim)
STOPPED Any pad touched while running White flash

Hold delay (default 500 ms) is configurable in Settings → Hold sensitivity.

Timer precision: Uses SystemClock.elapsedRealtimeNanos() which is monotonic and unaffected by clock adjustments. Centisecond display updates run at ~120 fps via a coroutine loop with delay(8ms).


Database Schema

solves table

Column Type Notes
id TEXT PK UUID
userId TEXT "local" until cloud sync
scramble TEXT Full 20-move WCA scramble
timeMs INTEGER Raw solve time in ms
penalty TEXT NONE / PLUS_2 / DNF
sessionName TEXT "default"
createdAt INTEGER Unix timestamp ms

profiles table

Column Type Notes
id TEXT PK UUID
username TEXT Max 20 chars
avatar TEXT Emoji string
createdAt INTEGER Unix timestamp ms
syncStatus TEXT "local" / "synced" / "pending"

Permissions

Permission Purpose
WAKE_LOCK Keep screen on during solving (WakeLockManager)
VIBRATE Haptic feedback on solve stop (HapticHelper)

Conversion Map (Web → Android)

Original web code Android equivalent
performance.now() + RAF SystemClock.elapsedRealtimeNanos() + coroutine
localStorage Room DB (solves, profile) + DataStore (settings)
CSS text-shadow LED glow Canvas BlurMaskFilter + setShadowLayer()
Pointer Events API awaitPointerEventScope + PointerId set
Wake Lock API PowerManager.WakeLock
navigator.vibrate() VibrationEffect.createOneShot()
TanStack Router Compose Navigation NavHost
Tailwind CSS Compose Modifier + drawBehind
oklch() colours Color(0xFFRRGGBB) ARGB constants
Google Fonts woff2 GMS Downloadable Fonts provider
crypto.randomUUID() java.util.UUID.randomUUID()

Known Notes

  • gradle-wrapper.jar is a binary blob that cannot be distributed as text. Android Studio will prompt you to regenerate it on first open, or run:
    gradle wrapper --gradle-version=8.6
  • The font_certs.xml file ships with placeholder certificate fragments. The full production certificates are embedded in the Google Play Services APK on device — the runtime will validate against those, not the values in this file. In practice the string-array just needs to exist; the system handles cert pinning.

VelocityTimer v1.0 — converted from CubeFlow (React/Vite) to Kotlin/Compose

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages