Architecture Overview¶
OpenTransition follows a clean architecture pattern with clear separation of concerns across three main layers: Data, Domain, and UI.
High-Level Architecture¶
┌─────────────────────────────────────────────────────────┐
│ UI Layer │
│ (Fragments, Activities, ViewModels, Adapters) │
│ - MainActivity │
│ - Various Fragments (Home, Gallery, Settings, etc.) │
│ - ViewBinding for UI │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Domain Layer │
│ (Business Logic, Use Cases, State Management) │
│ - HomeDomain │
│ - AssignPhotosDomain │
│ - SettingsDomain │
│ - AddEditMilestoneDomain │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Data Layer │
│ (Models, Database, Repository Pattern) │
│ - Realm Database │
│ - Photo & Milestone Models │
│ - Firebase Integration │
└─────────────────────────────────────────────────────────┘
Design Patterns¶
1. MVVM (Model-View-ViewModel)¶
OpenTransition uses a variant of MVVM with a Domain layer:
- Model: Data classes representing Photos, Milestones, etc.
- View: XML layouts and Fragments
- ViewModel: Domain classes that manage UI state and business logic
- Domain Layer: Acts as a ViewModel but is called "Domain" in this codebase
2. Repository Pattern¶
While not explicitly called repositories, the Domain classes act as repositories: - Abstract data access logic - Provide clean API for UI layer - Handle data transformations - Manage background operations
3. Reactive Programming¶
Uses RxJava 3 for: - Asynchronous operations - Event streams - State management - UI updates
4. Dependency Injection¶
Simple singleton pattern via DomainManager:
class DomainManager {
val homeDomain = HomeDomain()
val assignPhotosDomain = AssignPhotosDomain()
val settingsDomain = SettingsDomain()
val addEditMilestoneDomain = AddEditMilestoneDomain()
val editPhotoDomain = EditPhotoDomain()
}
Core Components¶
Application Class¶
OpenTransitionApp: Application singleton
- Initializes MobileAds
- Manages DomainManager
- Handles app version updates
- Manages Firebase settings synchronization
class OpenTransitionApp : Application() {
val domainManager = DomainManager()
val adConsentStatus = BehaviorSubject.createDefault(ConsentStatus.UNKNOWN)
val firebaseSettingUtil: FirebaseSettingUtil by lazy(LazyThreadSafetyMode.NONE) {
FirebaseSettingUtil()
}
}
Main Activity¶
MainActivity: Single activity architecture
- Hosts Navigation Component
- Manages media picker
- Handles Firebase Auth
- Manages screen lock/security
- Processes backup imports
Navigation¶
Uses Android Navigation Component: - Single Activity with multiple Fragments - Type-safe arguments with SafeArgs plugin - Global actions for common navigation patterns - Back stack management
Database¶
Primary: Room + SQLCipher (modern Android database, replaces Realm)
- SQLite-backed via Room DAO/Entity pattern
- Optional SQLCipher full-database encryption (AES-256)
- Encryption keys stored in Android Keystore
- Decoy vault: separate encrypted database for coercion protection
AppDatabase.kt— Room database with optional SQLCipher support
Legacy compatibility: Realm Kotlin (import-only)
- Realm data models kept solely for importing TransTracks
.ttbackupbackups - All Realm code is marked with
BACKWARDS COMPATIBILITYcomments RealmBackupImporter.kthandles migration to Room
Cloud Services¶
Firebase Integration: - Authentication: User accounts with email/Google sign-in - Firestore: Cloud data sync - Crashlytics: Crash reporting (release builds only) - Analytics: Usage tracking (opt-in)
Package Structure¶
mobile/src/main/java/com/shelbeely/opentransition/
├── data/ # Realm data models (import backwards compat)
│ ├── Photo.kt # Photo data model (Realm, import only)
│ ├── Milestone.kt # Milestone data model (Realm, import only)
│ └── TransTracksFileProvider.kt # Content provider
│
├── database/ # Room database layer
│ ├── AppDatabase.kt # Room database with optional SQLCipher
│ ├── DatabaseManager.kt # Real/decoy vault switching
│ ├── KeystoreManager.kt # Android Keystore key management
│ └── dao/ # Data Access Objects
│
├── domain/ # Business logic layer
│ ├── DomainManager.kt # Domain instances manager
│ ├── HomeDomain.kt # Home screen logic
│ ├── AssignPhotosDomain.kt # Photo assignment logic
│ ├── SettingsDomain.kt # Settings management
│ ├── AddEditMilestoneDomain.kt # Milestone editing
│ └── EditPhotoDomain.kt # Photo editing logic
│
├── ui/ # User interface layer
│ ├── MainActivity.kt # Main activity
│ ├── home/ # Home screen
│ ├── gallery/ # Gallery view
│ ├── milestones/ # Milestones management
│ ├── settings/ # Settings screen
│ ├── selectphoto/ # Photo selection
│ ├── assignphoto/ # Photo assignment
│ ├── editphoto/ # Photo editing
│ ├── singlephoto/ # Single photo view
│ ├── recordaudio/ # Audio recording
│ ├── lock/ # Lock screen
│ └── widget/ # Custom widgets (WaveformView, etc.)
│
├── camera/ # CameraX + ML Kit face detection
│
├── util/ # Utility classes
│ ├── FileUtil.kt # File operations
│ ├── RxSchedulers.kt # RxJava schedulers
│ ├── AudioPlayerManager.kt # Audio playback
│ ├── AudioAnalysisUtil.kt # Pitch/formant analysis
│ └── settings/ # Settings utilities
│ ├── SettingsManager.kt # Settings management
│ ├── Theme.kt # Theme definitions
│ ├── LockType.kt # Lock type enum
│ └── PrefUtil.kt # SharedPreferences helper
│
├── background/ # Background tasks
│ ├── CameraHandler.kt # Camera operations
│ └── StoragePermissionHandler.kt # Permission handling
│
├── wear/ # Wearable Data Layer integration
│
└── OpenTransitionApp.kt # Application class (singleton)
Data Flow¶
Typical User Action Flow¶
- User Interaction → Fragment receives input
- Fragment → Calls Domain method
- Domain → Updates Room database
- Room → Notifies observers via RxJava / Flow
- Domain → Emits state updates
- Fragment → Updates UI
Example: Adding a Photo¶
// 1. User selects photo in UI (Fragment)
pickMediaLauncher.launch(PickVisualMediaRequest(type))
// 2. MainActivity receives result
pickMediaLauncher = registerForActivityResult(PickMultipleVisualMedia()) { uris ->
// Navigate to assign photos
navController.navigate(MainNavDirections.actionGlobalAssignPhotos(...))
}
// 3. AssignPhotosFragment gets data
val args: AssignPhotosFragmentArgs by navArgs()
// 4. Domain processes the request
assignPhotosDomain.addPhotos(photoUris, type, date)
// 5. Domain writes to Room via DAO
val db = DatabaseManager.getDatabase(context)
db.photoDao().insert(Photo(...))
// 6. UI observes changes and updates
assignPhotosDomain.state
.observeOn(RxSchedulers.main())
.subscribe { state -> updateUI(state) }
Threading Model¶
RxSchedulers¶
Centralized scheduler management:
object RxSchedulers {
fun main(): Scheduler = AndroidSchedulers.mainThread()
fun io(): Scheduler = Schedulers.io()
fun computation(): Scheduler = Schedulers.computation()
}
Threading Rules¶
- UI updates: Main thread only
- Database operations: IO thread
- File operations: IO thread
- Network calls: IO thread
- Heavy computations: Computation thread
State Management¶
Domain State Pattern¶
Each Domain class manages its own state:
sealed class HomeState {
object Loading : HomeState()
data class Success(val photos: List<Photo>) : HomeState()
data class Error(val message: String) : HomeState()
}
State is exposed via RxJava observables:
class HomeDomain {
private val _state = BehaviorSubject.create<HomeState>()
val state: Observable<HomeState> = _state
fun loadPhotos() {
_state.onNext(HomeState.Loading)
// Load data...
_state.onNext(HomeState.Success(photos))
}
}
Error Handling¶
Layered Error Handling¶
- Data Layer: Catch database/network errors
- Domain Layer: Transform to domain errors
- UI Layer: Display user-friendly messages
Firebase Integration¶
- Crashlytics: Automatic crash reporting
- Analytics: Track error events
- Custom logging: Context-aware error messages
Security Architecture¶
App Lock System¶
Four lock types:
- Off: No lock
- Normal: Standard app icon, PIN/pattern lock
- Trains: Disguised app icon, PIN/pattern lock
- Biometric: Fingerprint, face, or iris scan via BiometricPrompt (BIOMETRIC_STRONG)
Lock implementation: - Hashed PIN/pattern storage - Configurable auto-lock delay - Secure flag prevents screenshots when locked - Biometric requires a backup passcode
Data Security¶
- Local storage: Room database with optional SQLCipher AES-256 encryption
- Key storage: Android Keystore (hardware-backed when available)
- Decoy vault: Separate encrypted database accessible with a different passcode
- Password hashing: Salted hash for PIN/pattern
- Secure flag: Prevents screenshots in sensitive screens
- Firebase Auth: Industry-standard authentication
Performance Optimizations¶
Image Loading¶
- Picasso: Efficient image loading and caching
- Memory management: Aggressive bitmap recycling
- Lazy loading: Load images on demand
Database Queries¶
- Reactive queries: Only load visible data
- Pagination: Limit query results
- Indexing: Proper field indexing in Realm
Memory Management¶
- LeakCanary: Memory leak detection (debug only)
- Lifecycle awareness: Proper subscription disposal
- CompositeDisposable: Batch disposal of RxJava subscriptions
Testing Architecture¶
Unit Tests¶
Location: mobile/src/test/
- Domain logic tests
- Utility function tests
- ViewModel tests
Instrumentation Tests¶
Location: mobile/src/androidTest/
- UI tests
- Database tests
- Integration tests
Next Steps¶
Learn more about specific layers:
- Data Layer - Models and persistence
- Domain Layer - Business logic
- UI Layer - User interface
- Navigation - App navigation flow