Skip to content

Initial implementation of Audio Journal Android app - #1

Merged
dschuld merged 18 commits into
mainfrom
claude/android-audio-recorder-bgx133
Jul 20, 2026
Merged

Initial implementation of Audio Journal Android app#1
dschuld merged 18 commits into
mainfrom
claude/android-audio-recorder-bgx133

Conversation

@dschuld

@dschuld dschuld commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

This PR introduces the complete initial implementation of Audio Journal, a simple Android voice recorder with automatic cloud upload to AWS S3. The app provides a minimal UI with a single record button, pause/resume controls, and background upload functionality.

Key Changes

Core Recording Engine

  • RecordingEngine: Pure Kotlin state machine managing recording lifecycle (idle → recording ↔ paused → idle) with elapsed time tracking that excludes paused periods
  • AudioRecorder interface with MediaRecorderAudioRecorder implementation producing AAC-encoded .m4a files
  • RecordingService: Foreground service keeping the microphone alive with ongoing notification, driven by intents from the UI

Cloud Upload

  • S3CloudUploader: Uploads recordings to AWS S3 using AWS SDK for Kotlin
  • UploadWorker: WorkManager-based background task with exponential backoff retry and network connectivity awareness
  • UploadScheduler: Queues uploads when recordings finish; handles offline scenarios gracefully
  • Configuration via BuildConfig fields populated from local.properties or environment variables at build time

UI Layer

  • RecorderScreen: Jetpack Compose UI with recording status, elapsed time display, and context-aware buttons (record/pause/resume/stop)
  • RecorderViewModel: Observes shared RecordingEngine state and provides elapsed time ticker
  • Permission handling for RECORD_AUDIO and POST_NOTIFICATIONS (Android 13+)
  • Time formatting utility supporting MM:SS and H:MM:SS formats

Storage & Configuration

  • RecordingStore: Generates collision-free timestamped filenames (recording_YYYY-MM-DD_HH-MM-SS.m4a)
  • AppContainer: Hand-rolled dependency injection holding singleton RecordingEngine and CloudUploader
  • S3Config: Immutable configuration with validation for required credentials

Testing

  • Unit tests for RecordingEngine with fake AudioRecorder and TimeSource
  • Unit tests for upload retry logic (UploadDecision)
  • Unit tests for time formatting and S3 configuration validation
  • UI smoke test verifying basic screen rendering

Notable Implementation Details

  • The RecordingEngine is pure Kotlin (no Android framework dependencies) for full testability; invalid state transitions are silently ignored to handle UI/service intent races
  • Elapsed time correctly excludes paused stretches by tracking recordedBeforePauseMillis and recordingSinceMillis
  • App works without S3 credentials configured—recordings stay local and UI indicates this
  • Foreground service uses ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE on Android Q+ for proper categorization
  • Gradle wrapper and build configuration included for reproducible builds

https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB

claude and others added 18 commits July 9, 2026 14:45
Single-screen Jetpack Compose voice recorder. Recording runs in a
foreground microphone service driven by a unit-tested pure-Kotlin state
machine. Finished .m4a files are saved to app storage and queued via
WorkManager for upload to AWS S3 (credentials from local.properties;
app degrades gracefully to local-only storage when unconfigured).
Includes JVM unit tests, a Compose UI smoke test, and a GitHub Actions
workflow that builds, lints, tests and publishes the debug APK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
The AWS SDK's transitive okhttp-android 5.4.0 dependency requires
compileSdk 36, which AGP 8.7.3 does not support.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
aws-sdk-kotlin 1.8.2 artifacts are compiled with Kotlin metadata 2.4.0,
which the 2.1.21 compiler cannot read (internal compiler error in CI).
Also migrate from the removed kotlinOptions DSL to compilerOptions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
Two AWS SDK transitive jars ship META-INF/versions/9/OSGI-INF/MANIFEST.MF,
which fails mergeDebugJavaResource. The OSGi metadata is unused on Android.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
Google Drive is now the default upload destination. Auth uses Play
services' AuthorizationClient, so no secrets are embedded in the APK;
the user connects their Google account in-app. Uploads go through a
minimal Drive v3 REST client, with destination folders resolved by name
(find-or-create), so an existing folder is reused. A configurable
folder list (drive.folders) drives an in-app picker so each recording
can target a different folder; the first entry is the default.

The S3 backend is kept and selectable via upload.backend=s3, with the
folder choice mapped to an S3 key prefix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
Gradle 8.14.3 -> 9.6.1, AGP 8.10.1 -> 9.2.0, Kotlin 2.3.21 -> 2.4.0,
Compose BOM 2025.05.00 -> 2026.06.01, and latest stable releases of
androidx core/lifecycle/activity/work/test, coroutines, serialization,
AWS SDK, and play-services-auth. material-icons-extended is now pinned
explicitly since recent Compose BOMs no longer manage the frozen icons
artifacts. CI moves to JDK 21 and current action majors (checkout@v6,
setup-java@v5, gradle/actions@v6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
AGP 9 applies Kotlin itself and errors when the standalone
org.jetbrains.kotlin.android plugin is also applied. Drop that plugin,
let jvmTarget default from compileOptions.targetCompatibility, and pin
the Compose compiler plugin to Kotlin 2.3.10 to match the Kotlin
version bundled with AGP 9.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
Current androidx stable releases (core 1.19.0, lifecycle 2.11.0)
require compiling against API 37.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
Google authorizes the app by package name + signing certificate SHA-1.
CI runners generate a fresh random debug keystore per build, so artifact
APKs could never match the OAuth client registration. Restore the
owner's keystore from the DEBUG_KEYSTORE_B64 secret before building so
CI APKs carry the registered certificate; without the secret the build
still works, minus Google sign-in in the artifact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
Check the secret via an env variable inside the script instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
upload-artifact@v4 artifacts are immutable within a run by default, so
re-running jobs (e.g. after updating the signing keystore secret) kept
serving the stale APK from the first attempt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
AGP resolved its default debug keystore to a different location on the
CI runner, so the keystore written to ~/.android was ignored and every
run signed with a fresh random certificate. Point the debug signing
config at ci-debug.keystore in the repo root when that file exists
(decoded from the DEBUG_KEYSTORE_B64 secret in CI, gitignored, absent
in local builds), and print its fingerprint in the CI log for easy
verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
local.properties does not exist on CI runners, so artifact APKs always
fell back to the default folder name. The build config already reads
the DRIVE_FOLDERS env var; wire it to a repository variable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
Folder names are ambiguous in Drive (duplicates are allowed), so
destinations are now configured as Label=folderId entries; the label
feeds the in-app picker and the S3 key prefix, the id is used directly
as the upload parent (find-or-create by name is removed). An empty
configuration uploads to the My Drive root. supportsAllDrives is set on
the upload so shared-drive folder ids work too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
Folder names are ambiguous in Drive (duplicates are allowed), so
destinations are now configured as Label=folderId entries; the label
feeds the in-app picker and the S3 key prefix, the id is used directly
as the upload parent (find-or-create by name is removed). An empty
configuration uploads to the My Drive root. supportsAllDrives is set on
the upload so shared-drive folder ids work too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvPfCEeJ3tvSAje8PnEfPB
@dschuld
dschuld merged commit 5f2168d into main Jul 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants