diff --git a/.gitignore b/.gitignore
index d8b62a43..bdd0afb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,5 +42,10 @@ Rev.md
.DS_Store
Thumbs.db
MESHIFY_KEYSTORE_BASE64.txt
+.agents
+AGENT.md
+AGENTS.md
+GEMINI.md
+*.salive
gha-creds-*.json
diff --git a/AGENT.md b/AGENT.md
deleted file mode 100644
index 91dc404d..00000000
--- a/AGENT.md
+++ /dev/null
@@ -1,351 +0,0 @@
-# MESHIFY PROJECT CONTEXT
-
-> **Project**: Meshify - Offline P2P Messaging
- - This entire project is built using LLMs.
- - This app is for personal use only, so performance and speed should be prioritized over security, but clean code is essential.
- - The current project is a Git repo, so you can look at the latest commits to understand what changes have occurred.
-> **Type**: Android Application (Kotlin + Jetpack Compose + Hilt)
-> **Architecture**: Clean Architecture + MVVM + Multi-Module
-
----
-
-## PROJECT OVERVIEW
-
-Meshify is a decentralized, offline-first P2P messaging application that enables communication between Android devices on the same local network without requiring internet connectivity or central servers.
-
-### Core Value Proposition
-- **Simple & Lightweight**: Plaintext messages over LAN with zero encryption overhead
-- **Offline-Ready**: Works on local networks (WiFi) without internet access
-- **Clean Architecture**: Modular, testable, and maintainable codebase
-- **Localization**: Full English and Arabic (RTL) support
-
-### Key Features
-- 1-on-1 plaintext text messaging
-- File attachments (images, videos, documents)
-- Message replies, reactions, delete, forward
-- mDNS/NSD peer discovery
-- LAN TCP transport with connection pooling
-- Material 3 UI
-- Room offline database with pagination
-- UUID-based peer identification (SimplePeerIdProvider)
-
----
-
-## ARCHITECTURE
-
-### Module Dependency Graph
-
-```
-:app
- ├── :core:common # Utilities (Logger, FileUtils, ImageCompressor)
- ├── :core:data # Room DB, DataStore, Repository implementations
- ├── :core:domain # Pure Kotlin: Interfaces, Models, Use Cases
- ├── :core:network # mDNS, Sockets, LAN/BLE Transport
- └── :core:ui # Material 3 Components, Theme, Shared UI
- ├── :feature:home # Recent chats screen
- ├── :feature:chat # Chat conversation screen
- ├── :feature:discovery # Device discovery
- └── :feature:settings # Settings & customization
-```
-
-### Dependency Rules
-```
-:app → :feature:* → :core:*
-:feature:* → :core:* (NEVER other feature modules)
-:core:* → :core:domain (domain has ZERO dependencies)
-```
-
-### Architectural Layers
-
-**Presentation Layer** (`:feature:*`, `:core:ui`)
-- Jetpack Compose UI with Material 3
-- ViewModels for state management
-- Hilt for dependency injection
-
-**Domain Layer** (`:core:domain`)
-- Pure Kotlin (no Android/framework dependencies)
-- Repository interfaces (`IChatRepository`, `ISettingsRepository`)
-- Domain models and use cases
-- Security models (`SecurityEvent`, `MessageEnvelope`, `OobVerificationMethod`)
-
-**Data Layer** (`:core:data`)
-- Room database (plain SQLite, no encryption)
-- DataStore for preferences
-- Repository implementations
-- LRU caching and pagination
-
-**Network Layer** (`:core:network`)
-- LAN Transport: TCP sockets with connection pooling
-- BLE Transport: Bluetooth Low Energy (in progress)
-- mDNS/NSD service discovery
-- Keep-alive and health monitoring
-
----
-
-## TECH STACK
-
-### Core Technologies
-| Technology | Version | Purpose |
-|------------|---------|---------|
-| **Kotlin** | 2.3.10 | Primary language |
-| **AGP** | 9.1.0 | Android Gradle Plugin |
-| **Jetpack Compose** | 2026.02.00 (BOM) | UI toolkit |
-| **Material 3** | 1.4.0-alpha10 | Design system |
-
-### Data & State
-| Library | Version | Purpose |
-|---------|---------|---------|
-| **Room** | 2.8.4 | Local database |
-| **DataStore** | 1.1.1 | Preferences storage |
-| **Paging 3** | 3.3.5 | Data pagination |
-| **Navigation** | 2.9.7 | In-app navigation |
-| **Hilt** | 2.59 | Dependency injection |
-
-### Networking & Media
-| Library | Version | Purpose |
-|---------|---------|---------|
-| **Media3** | 1.8.0 | Media playback |
-| **Coil 3** | 3.4.0 | Image loading |
-
-### Build & Testing
-| Tool | Purpose |
-|------|---------|
-| **KSP** | Annotation processing (Room, Hilt) |
-| **JUnit 4** | Unit testing |
-| **MockK** | Mocking framework |
-| **Turbine** | Flow testing |
-| **Robolectric** | Android unit testing |
-| **Espresso** | UI testing |
-
----
-
-## BUILD & RUN COMMANDS
-
-### Prerequisites
-- **JDK**: 21 (configured via SDKMAN: `/home/youusef/.sdkman/candidates/java/21-librca`)
-- **Android SDK**: API 26+ (Min), API 35 (Target)
-- **Gradle JVM Args**: `-Xmx4096m` (configured in `gradle.properties`)
-
-### Build Commands
-
-```bash
-# Clean build
-./gradlew clean
-
-# Build debug APK
-./gradlew assembleDebug
-
-# Build release APK
-./gradlew assembleRelease
-
-# Run all tests
-./gradlew test
-
-# Run unit tests only
-./gradlew testDebugUnitTest
-
-# Run instrumented tests
-./gradlew connectedAndroidTest
-
-# Lint check
-./gradlew lint
-
-# Format Kotlin code
-./gradlew ktlintFormat
-```
-
-### Development Commands
-
-```bash
-# Run app on connected device/emulator
-./gradlew installDebug
-
-# View build scan
-./gradlew build --scan
-
-# Check dependency updates
-./gradlew dependencyUpdates
-```
-
----
-
-## KEY DIRECTORIES
-
-```
-Meshify/
-├── app/ # Main application module
-│ ├── src/main/java/com/p2p/meshify/
-│ │ ├── MeshifyApp.kt # Application class (Hilt setup)
-│ │ ├── MainActivity.kt # Main activity (Compose host)
-│ │ ├── service/ # Foreground services
-│ │ └── receivers/ # Broadcast receivers
-│ ├── schemas/ # Room database schemas
-│ └── build.gradle.kts # App-level build config
-├── core/
-│ ├── common/ # Shared utilities
-│ ├── data/ # Data layer (Room, DataStore, Repos)
-│ ├── domain/ # Domain layer (Interfaces, Models)
-│ │ ├── model/ # Domain models (PeerDevice, Payload, etc.)
-│ │ ├── repository/ # Repository interfaces
-│ │ ├── security/ # Crypto interfaces & models
-│ │ └── usecase/ # Use cases
-│ ├── network/ # Network layer
-│ │ ├── base/ # Transport interfaces
-│ │ ├── lan/ # LAN TCP implementation
-│ │ └── ble/ # BLE implementation
-│ └── ui/ # Shared UI components & theme
-├── feature/
-│ ├── home/ # Recent chats screen
-│ ├── chat/ # Chat conversation screen
-│ ├── discovery/ # Peer discovery screen
-│ ├── settings/ # Settings screen
-│ ├── onboarding/ # Welcome flow
-│ └── help/ # FAQ & About screens
-├── gradle/
-│ └── libs.versions.toml # Version catalog
-├── docs/ # Documentation local only (this file in the .gitignore)
-└── QWEN.md # This file (AI assistant context) (this file in the .gitignore)
-```
-
----
-
-## SECURITY ARCHITECTURE
-
-### Message Format — Plaintext
-- **MessageEnvelope**: Simple data class with `senderId`, `recipientId`, `text`, `timestamp`, `messageType`
-- **Serialization**: Binary ByteBuffer with length-prefixed fields (efficient, compact)
-- **No encryption**: Messages travel as plaintext over LAN
-- **No authentication**: No ECDSA signatures, no TOFU, no key exchange
-
-### Database
-- **Plain SQLite**: No SQLCipher, no encryption
-- **Room**: Version 7 (migrated from v6 by dropping `trusted_peers` table)
-- **Key Management**: None required
-
-### Network
-- **No Central Server**: Pure P2P architecture
-- **Local Network Only**: No internet dependency
-- **Connection Pooling**: Pre-warmed, monitored connections
-- **Handshake V3**: Exchanges peer name only (no crypto fields)
-
-### Peer Identity
-- **SimplePeerIdProvider**: UUID generated once, stored in SharedPreferences
-- **No Keystore**: No EC keys, no certificates, no Android Keystore
-- **No Biometric**: No fingerprint/face authentication
-
-### Removed Security Infrastructure (5 phases, ~8,300 lines)
-All of the following have been permanently removed:
-- ~~SQLCipher~~ (Phase 1)
-- ~~Android Keystore PeerIdentity~~ (Phase 2)
-- ~~ECDH key exchange (EcdhSessionManager)~~ (Phase 3)
-- ~~AES-256-GCM encryption (MessageEnvelopeCrypto)~~ (Phase 3)
-- ~~HKDF key derivation (HkdfKeyDerivation)~~ (Phase 3)
-- ~~Nonce cache replay protection~~ (Phase 3)
-- ~~EncryptedSessionKeyStore~~ (Phase 3)
-- ~~TOFU trust model (PeerTrustStore, TrustedPeer, TrustLevel)~~ (Phase 4)
-- ~~Security events (DecryptionFailed, TofuViolation, SessionExpired)~~ (Phase 4)
-- ~~Tink, BouncyCastle, Security Crypto, Biometric dependencies~~ (Phase 5)
-
----
-
-## UI/UX CONVENTIONS
-
-### Material 3
-- Uses `androidx.compose.material3:material3:1.4.0-alpha10`
-- Opt-in: `@OptIn(ExperimentalMaterial3ExpressiveApi::class)`
-- Motion presets: Gentle, Standard, Snappy, Bouncy
-
-### Theme System
-- **Modes**: Light, Dark, System
-- **Dynamic Colors**: Material You support
-- **Custom Seed Colors**: User-selectable accent colors
-- **Persistence**: Stored via DataStore
-
-### Localization
-- **Languages**: English, Arabic
-- **RTL Support**: Full right-to-left layout support
-- **String Resources**: All UI strings in `strings.xml`
-- **Google Fonts**: For typography
-
-## UI/UX CONVENTIONS (FIDGET TOY PHILOSOPHY)
-
-### Core Philosophy
-- **The "Fidget Toy" Feel:** Every user interaction MUST have an immediate, satisfying, and physically plausible reaction. The app should feel alive, tactile, and playful.
-- **Zero Latency Illusion:** Since the app is offline-first, UI updates must be instant. Never block UI animations waiting for background tasks.
-
-### Design Language & Shapes
-- **Standard:** Material You 3 Expressive (M3E) / Android 16 design language.
-- **Strict Shape Rules:**
- - **NEVER** use perfect `CircleShape` or hard sharp corners.
- - **ALWAYS** use **Squircles** or highly rounded squares (e.g., `RoundedCornerShape(24.dp)` or `28.dp` for large elements, `16.dp` for smaller ones).
- - **Shape Morphing:** Interactive elements MUST animate their corner radius or border thickness on press/hover (e.g., a button morphs from `24.dp` to `32.dp` when pressed down).
-
----
-
-### Key Optimizations
-- BufferedOutputStream for file transfer
-- WebP image compression
-- Parallel file transfer
-- Connection pooling
-- ArrayDeque for messages (O(1) prepend)
-- LRU cache for attachments
-- `derivedStateOf` in Compose
-- Stable LazyColumn keys
-- Flow `.distinctUntilChanged`
-
----
-
-## TESTING STRATEGY
-
-### Current Status
-- **Unit tests**: Minimal (needs coverage)
-- **UI tests**: Not implemented
-- **Test infrastructure**: JUnit, MockK, Turbine, Robolectric configured
-
-### Testing Conventions
-- **Unit Tests**: `/src/test/java/` (JUnit 4 + MockK)
-- **Instrumented Tests**: `/src/androidTest/java/` (AndroidX Test + Espresso)
-- **Flow Testing**: Use Turbine for Kotlin Flow streams
-- **Coroutines**: Use `kotlinx-coroutines-test` for dispatcher control
-
-### Test Targets
-- Repository implementations
-- ViewModel state management
-- UI component rendering
-- Integration tests (network + DB)
-- Message serialization/deserialization
-
----
-
-
-## DEVELOPMENT CONVENTIONS
-
-### Kotlin Coding Style
-- Use meaningful variable and function names
-- Add KDoc comments for public APIs
-- Prefer `val` over `var` (immutability first)
-- Use data classes for models
-- Seal classes for state modeling
-
-### Compose Best Practices
-- Use `remember` and `derivedStateOf` for performance
-- Stable keys for LazyColumn items
-- Hoist state to ViewModels
-- Use `LaunchedEffect` for side effects
-- Avoid recomposition loops
-
----
-
-## CONFIGURATION FILES
-
-| File | Purpose |
-|------|---------|
-| `build.gradle.kts` | Root build configuration |
-| `settings.gradle.kts` | Module includes and repository config |
-| `gradle.properties` | Gradle JVM args, Kotlin settings |
-| `gradle/libs.versions.toml` | Version catalog for dependencies |
-| `app/build.gradle.kts` | App-level build config (signing, features) |
-| `app/proguard-rules.pro` | R8/ProGuard rules |
-| `app/src/main/AndroidManifest.xml` | Android manifest (permissions, components) |
-
-
diff --git a/app/src/main/java/com/p2p/meshify/MainActivity.kt b/app/src/main/java/com/p2p/meshify/MainActivity.kt
index dd54cfdd..3ecc04f9 100644
--- a/app/src/main/java/com/p2p/meshify/MainActivity.kt
+++ b/app/src/main/java/com/p2p/meshify/MainActivity.kt
@@ -2,6 +2,7 @@ package com.p2p.meshify
import android.Manifest
import android.content.pm.PackageManager
+import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
@@ -25,13 +26,10 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController
import com.p2p.meshify.core.util.Logger
-import com.p2p.meshify.domain.model.FontFamilyPreset
-import com.p2p.meshify.domain.model.MotionPreset
+
import com.p2p.meshify.service.MeshForegroundService
-import com.p2p.meshify.core.ui.components.PremiumNoiseTexture
import com.p2p.meshify.core.ui.navigation.MeshifyNavHost
import com.p2p.meshify.core.ui.navigation.Screen
-import com.p2p.meshify.core.ui.theme.MD3EFontFamilies
import com.p2p.meshify.core.ui.theme.MeshifyTheme
import com.p2p.meshify.core.ui.hooks.rememberPremiumHaptics
import com.p2p.meshify.core.ui.hooks.LocalPremiumHaptics
@@ -48,12 +46,13 @@ import com.p2p.meshify.feature.settings.DeveloperViewModel
import com.p2p.meshify.feature.realdevicetesting.ui.RealDeviceTestingViewModel
import com.p2p.meshify.feature.realdevicetesting.ui.RealDeviceTestScreen
import com.p2p.meshify.feature.onboarding.WelcomeScreen
+import com.p2p.meshify.feature.onboarding.PermissionStatus
+import com.p2p.meshify.feature.onboarding.PermissionRequestResult
import com.p2p.meshify.feature.onboarding.WelcomeViewModel
import com.p2p.meshify.feature.onboarding.PermissionSummaryDialog
import com.p2p.meshify.feature.onboarding.PermissionRequestCard
import com.p2p.meshify.feature.onboarding.SkipConfirmationDialog
import com.p2p.meshify.feature.onboarding.PermissionDefinitions
-import com.p2p.meshify.feature.onboarding.PermissionRequestResult
import com.p2p.meshify.core.domain.interfaces.WifiStateChecker
import com.p2p.meshify.core.data.local.MeshifyDatabase
import androidx.hilt.navigation.compose.hiltViewModel
@@ -120,12 +119,32 @@ class MainActivity : ComponentActivity() {
onboardingPermissionLauncher.launch(permissions.toTypedArray())
}
+ private fun applyLocale(language: String) {
+ val locale = java.util.Locale.forLanguageTag(language)
+ java.util.Locale.setDefault(locale)
+ val config = Configuration(resources.configuration)
+ config.setLocale(locale)
+ resources.updateConfiguration(config, resources.displayMetrics)
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val app = application as MeshifyApp
+ // Apply stored locale before setContent so strings render in the right language.
+ // Use lifecycleScope.launch instead of runBlocking to avoid blocking the main thread.
+ lifecycleScope.launch {
+ try {
+ val lang = app.settingsRepository.appLanguage.first()
+ applyLocale(lang)
+ } catch (e: Exception) {
+ Logger.e("MainActivity -> Failed to load language", e)
+ // Default locale will be used as fallback
+ }
+ }
+
// Only request permissions immediately if onboarding was already completed.
// Otherwise, permissions will be requested after the onboarding flow.
lifecycleScope.launch {
@@ -149,12 +168,6 @@ class MainActivity : ComponentActivity() {
val settingsRepo = app.settingsRepository
val themeMode by settingsRepo.themeMode.collectAsState(initial = com.p2p.meshify.domain.repository.ThemeMode.SYSTEM)
val dynamicColor by settingsRepo.dynamicColorEnabled.collectAsState(initial = true)
- val motionPreset by settingsRepo.motionPreset.collectAsState(initial = MotionPreset.STANDARD)
- val motionScale by settingsRepo.motionScale.collectAsState(initial = 1.0f)
- val fontFamilyPreset by settingsRepo.fontFamilyPreset.collectAsState(initial = FontFamilyPreset.ROBOTO)
- val shapeStyle by settingsRepo.shapeStyle.collectAsState(initial = com.p2p.meshify.domain.model.ShapeStyle.CIRCLE)
- val bubbleStyle by settingsRepo.bubbleStyle.collectAsState(initial = com.p2p.meshify.domain.model.BubbleStyle.ROUNDED)
- val visualDensity by settingsRepo.visualDensity.collectAsState(initial = 1.0f)
val seedColorInt by settingsRepo.seedColor.collectAsState(initial = 0xFF006D68.toInt())
var isReady by remember { mutableStateOf(false) }
@@ -184,12 +197,6 @@ class MainActivity : ComponentActivity() {
MeshifyTheme(
themeMode = themeMode.name,
dynamicColor = dynamicColor,
- motionPreset = motionPreset,
- motionScale = motionScale,
- fontFamily = MD3EFontFamilies.getFontFamily(fontFamilyPreset),
- shapeStyle = shapeStyle,
- bubbleStyle = bubbleStyle,
- visualDensity = visualDensity,
seedColor = seedColor
) {
CompositionLocalProvider(LocalPremiumHaptics provides premiumHaptics) {
@@ -212,9 +219,6 @@ class MainActivity : ComponentActivity() {
Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background))
} else {
Box(modifier = Modifier.fillMaxSize()) {
- // High-end tactile feel
- PremiumNoiseTexture(alpha = 0.03f)
-
Surface(
modifier = Modifier.fillMaxSize(),
color = Color.Transparent
@@ -252,7 +256,7 @@ class MainActivity : ComponentActivity() {
DiscoveryScreen(
viewModel = discoveryViewModel,
onPeerClick = { peer -> navController.navigate(Screen.Chat(peer.id, peer.name)) },
- onSettingsClick = { navController.navigate(Screen.Settings) }
+ onBackClick = { navController.popBackStack() }
)
},
onChatRoute = { peerId, peerName ->
@@ -413,7 +417,7 @@ private fun OnboardingRoute(
val context = LocalContext.current
// Language: "en" or "ar"
- var currentLang by remember { mutableStateOf("en") }
+ val currentLang by settingsRepository.appLanguage.collectAsState(initial = "en")
// Permission flow state
var isPermissionFlowActive by remember { mutableStateOf(false) }
@@ -450,71 +454,88 @@ private fun OnboardingRoute(
}
}
- // WelcomeScreen
- WelcomeScreen(
- viewModel = onboardingViewModel,
- currentLang = currentLang,
- onLangChange = { newLang ->
- currentLang = newLang
- },
- onNextClick = {
- // Page 3 "Get Started" → start permission flow
- isPermissionFlowActive = true
- currentPermissionIndex = 0
- onboardingViewModel.startPermissionFlow()
- },
- onSkipClick = {
- if (isPermissionFlowActive) {
- showSkipConfirm = true
- } else {
+ Box(modifier = Modifier.fillMaxSize()) {
+ WelcomeScreen(
+ viewModel = onboardingViewModel,
+ currentLang = currentLang,
+ onLangChange = { newLang ->
scope.launch {
- settingsRepository.setOnboardingCompleted()
+ settingsRepository.setAppLanguage(newLang)
+ activity.recreate()
+ }
+ },
+ permissionStatuses = permissions.associate {
+ val res = permissionResults[it.id]
+ it.id to when (res) {
+ PermissionRequestResult.Granted -> PermissionStatus.Granted
+ PermissionRequestResult.Denied -> PermissionStatus.Denied
+ PermissionRequestResult.DeniedPermanently -> PermissionStatus.DeniedPermanently
+ else -> PermissionStatus.NotAsked
+ }
+ },
+ onNextClick = {
+ // Page 3 "Get Started" → start permission flow
+ isPermissionFlowActive = true
+ currentPermissionIndex = 0
+ },
+ onSkipClick = {
+ if (isPermissionFlowActive) {
+ showSkipConfirm = true
+ } else {
+ scope.launch {
+ settingsRepository.setOnboardingCompleted()
+ }
+ onNavigateToHome()
}
- onNavigateToHome()
}
- }
- )
+ )
- // Auto-advance after permission result
- LaunchedEffect(advanceTrigger) {
- if (advanceTrigger > 0) {
- kotlinx.coroutines.delay(PERMISSION_EXIT_ANIMATION_DELAY_MS)
- currentPermissionIndex++
- }
- }
+ // Permission flow: show cards one by one
+ if (isPermissionFlowActive && currentPermissionIndex < permissions.size) {
+ val perm = permissions[currentPermissionIndex]
- // Permission flow: show cards one by one
- if (isPermissionFlowActive && currentPermissionIndex < permissions.size) {
- val perm = permissions[currentPermissionIndex]
+ // Check if already granted
+ val alreadyGranted = perm.androidPermissions.all { pid ->
+ android.content.pm.PackageManager.PERMISSION_GRANTED ==
+ context.checkSelfPermission(pid)
+ }
- // Check if already granted
- val alreadyGranted = perm.androidPermissions.all { pid ->
- android.content.pm.PackageManager.PERMISSION_GRANTED ==
- context.checkSelfPermission(pid)
+ if (alreadyGranted) {
+ LaunchedEffect(perm.id) {
+ permissionResults[perm.id] = PermissionRequestResult.Granted
+ advanceTrigger++
+ }
+ } else {
+ PermissionRequestCard(
+ permission = perm,
+ onAllowClick = {
+ onRequestPermissions(perm.androidPermissions)
+ },
+ onDenyClick = {
+ permissionResults[perm.id] = PermissionRequestResult.Denied
+ advanceTrigger++
+ },
+ onRequestDismiss = {
+ isPermissionFlowActive = false
+ showSummaryDialog = true
+ }
+ )
+ }
}
- if (alreadyGranted) {
- LaunchedEffect(perm.id) {
- permissionResults[perm.id] = PermissionRequestResult.Granted
- advanceTrigger++
- kotlinx.coroutines.delay(PERMISSION_ALREADY_GRANTED_DISPLAY_DELAY_MS)
- currentPermissionIndex++
- }
- } else {
- PermissionRequestCard(
- permission = perm,
- onAllowClick = {
- onRequestPermissions(perm.androidPermissions)
- },
- onDenyClick = {
- permissionResults[perm.id] = PermissionRequestResult.Denied
+ // Auto-advance after permission result
+ LaunchedEffect(advanceTrigger) {
+ if (advanceTrigger > 0) {
+ kotlinx.coroutines.delay(PERMISSION_EXIT_ANIMATION_DELAY_MS)
+ if (currentPermissionIndex < permissions.size) {
currentPermissionIndex++
- },
- onRequestDismiss = {
+ }
+ // If we reached the end, show summary
+ if (currentPermissionIndex >= permissions.size) {
isPermissionFlowActive = false
showSummaryDialog = true
}
- )
+ }
}
}
diff --git a/app/src/main/java/com/p2p/meshify/di/AppModule.kt b/app/src/main/java/com/p2p/meshify/di/AppModule.kt
index e3352c8b..8f48a934 100644
--- a/app/src/main/java/com/p2p/meshify/di/AppModule.kt
+++ b/app/src/main/java/com/p2p/meshify/di/AppModule.kt
@@ -36,7 +36,7 @@ object AppModule {
return Room.databaseBuilder(context, MeshifyDatabase::class.java, "meshify.db")
.addMigrations(migration5to6, MeshifyDatabase.MIGRATION_6_7)
- .fallbackToDestructiveMigration(dropAllTables = true)
+ .fallbackToDestructiveMigration()
.build()
}
diff --git a/core/common/src/main/res/values/strings.xml b/core/common/src/main/res/values/strings.xml
index e49b71b2..1ed0b264 100644
--- a/core/common/src/main/res/values/strings.xml
+++ b/core/common/src/main/res/values/strings.xml
@@ -322,6 +322,13 @@
Message Image
Message Status
+ Message queued to send
+ Message sending
+ Message sent
+ Message delivered
+ Message received
+ Message read
+ Message failed to send
Message Reaction
@@ -629,4 +636,15 @@
Backup imported successfully
Failed to import backup
+
+
+ Staged media attachment
+ Video play icon
+ Remove attachment
+ No devices discovered
+ Open Wi-Fi settings
+ Image attachment
+ Error
+ Retry
+ QR Code for verification
diff --git a/core/data/src/main/java/com/p2p/meshify/core/data/repository/ChatRepositoryImpl.kt b/core/data/src/main/java/com/p2p/meshify/core/data/repository/ChatRepositoryImpl.kt
index 6ef97e28..38506882 100644
--- a/core/data/src/main/java/com/p2p/meshify/core/data/repository/ChatRepositoryImpl.kt
+++ b/core/data/src/main/java/com/p2p/meshify/core/data/repository/ChatRepositoryImpl.kt
@@ -416,7 +416,17 @@ class ChatRepositoryImpl(
}
val mediaBytes = withContext(Dispatchers.IO) {
- mediaFile.readBytes()
+ // Use streaming read with 8KB buffer instead of file.readBytes()
+ // to avoid loading the entire file into memory at once.
+ val outputStream = java.io.ByteArrayOutputStream(mediaFile.length().coerceAtMost(Int.MAX_VALUE.toLong()).toInt())
+ java.io.BufferedInputStream(java.io.FileInputStream(mediaFile)).use { inputStream ->
+ val buffer = ByteArray(8192)
+ var bytesRead: Int
+ while (inputStream.read(buffer).also { bytesRead = it } != -1) {
+ outputStream.write(buffer, 0, bytesRead)
+ }
+ }
+ outputStream.toByteArray()
}
val myId = settingsRepository.getDeviceId()
diff --git a/core/data/src/main/java/com/p2p/meshify/core/data/repository/PendingMessageRepository.kt b/core/data/src/main/java/com/p2p/meshify/core/data/repository/PendingMessageRepository.kt
index 4b535f4a..e17e8a98 100644
--- a/core/data/src/main/java/com/p2p/meshify/core/data/repository/PendingMessageRepository.kt
+++ b/core/data/src/main/java/com/p2p/meshify/core/data/repository/PendingMessageRepository.kt
@@ -144,7 +144,17 @@ class PendingMessageRepository(
Logger.e("PendingMessageRepository -> Media file not found for retry: $path")
return Result.failure(Exception("Media file not found: $path"))
}
- file.readBytes()
+ // Use streaming read with 8KB buffer instead of file.readBytes()
+ // to avoid loading the entire file into memory at once.
+ val outputStream = java.io.ByteArrayOutputStream(file.length().coerceAtMost(Int.MAX_VALUE.toLong()).toInt())
+ java.io.BufferedInputStream(java.io.FileInputStream(file)).use { inputStream ->
+ val buffer = ByteArray(8192)
+ var bytesRead: Int
+ while (inputStream.read(buffer).also { bytesRead = it } != -1) {
+ outputStream.write(buffer, 0, bytesRead)
+ }
+ }
+ outputStream.toByteArray()
} else {
Logger.w("PendingMessageRepository -> No media path for message ${msg.id}")
byteArrayOf()
diff --git a/core/data/src/main/java/com/p2p/meshify/core/data/repository/SettingsRepository.kt b/core/data/src/main/java/com/p2p/meshify/core/data/repository/SettingsRepository.kt
index c4524869..2db163c8 100644
--- a/core/data/src/main/java/com/p2p/meshify/core/data/repository/SettingsRepository.kt
+++ b/core/data/src/main/java/com/p2p/meshify/core/data/repository/SettingsRepository.kt
@@ -8,10 +8,6 @@ import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.p2p.meshify.core.util.Logger
-import com.p2p.meshify.domain.model.BubbleStyle
-import com.p2p.meshify.domain.model.FontFamilyPreset
-import com.p2p.meshify.domain.model.MotionPreset
-import com.p2p.meshify.domain.model.ShapeStyle
import com.p2p.meshify.domain.model.TransportMode
import com.p2p.meshify.domain.repository.ISettingsRepository
import com.p2p.meshify.domain.repository.ThemeMode
@@ -25,10 +21,6 @@ import java.util.UUID
private val Context.dataStore by preferencesDataStore(name = "settings")
-/**
- * Data layer implementation of Settings Repository.
- * Extended for MD3E - Central Source of Truth for all design variables.
- */
class SettingsRepository(private val context: Context) : ISettingsRepository {
companion object {
@@ -39,25 +31,13 @@ class SettingsRepository(private val context: Context) : ISettingsRepository {
val KEY_HAPTIC_FEEDBACK = booleanPreferencesKey("haptic_feedback")
val KEY_NETWORK_VISIBLE = booleanPreferencesKey("network_visible")
val KEY_AVATAR_HASH = stringPreferencesKey("avatar_hash")
-
- // MD3E Settings Keys
- val KEY_SHAPE_STYLE = stringPreferencesKey("shape_style")
- val KEY_MOTION_PRESET = stringPreferencesKey("motion_preset")
- val KEY_MOTION_SCALE = floatPreferencesKey("motion_scale")
- val KEY_FONT_FAMILY = stringPreferencesKey("font_family")
- val KEY_CUSTOM_FONT_URI = stringPreferencesKey("custom_font_uri")
- val KEY_BUBBLE_STYLE = stringPreferencesKey("bubble_style")
- val KEY_VISUAL_DENSITY = floatPreferencesKey("visual_density")
val KEY_SEED_COLOR = intPreferencesKey("seed_color")
- // BLE Transport Settings Keys
val KEY_BLE_ENABLED = booleanPreferencesKey("ble_enabled")
val KEY_TRANSPORT_MODE = stringPreferencesKey("transport_mode")
- // Onboarding Settings Keys
val KEY_ONBOARDING_COMPLETED = booleanPreferencesKey("onboarding_completed")
- // New Settings Keys
val KEY_APP_LANGUAGE = stringPreferencesKey("app_language")
val KEY_FONT_SIZE_SCALE = floatPreferencesKey("font_size_scale")
val KEY_NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled")
@@ -93,58 +73,12 @@ class SettingsRepository(private val context: Context) : ISettingsRepository {
preferences[KEY_AVATAR_HASH]
}
- // MD3E Settings Flows
- override val shapeStyle: Flow = context.dataStore.data.map { preferences ->
- try {
- ShapeStyle.valueOf(preferences[KEY_SHAPE_STYLE] ?: "CIRCLE")
- } catch (e: Exception) {
- ShapeStyle.CIRCLE
- }
- }
-
- override val motionPreset: Flow = context.dataStore.data.map { preferences ->
- try {
- MotionPreset.valueOf(preferences[KEY_MOTION_PRESET] ?: "STANDARD")
- } catch (e: Exception) {
- MotionPreset.STANDARD
- }
- }
-
- override val motionScale: Flow = context.dataStore.data.map { preferences ->
- preferences[KEY_MOTION_SCALE] ?: 1.0f
- }
-
- override val fontFamilyPreset: Flow = context.dataStore.data.map { preferences ->
- try {
- FontFamilyPreset.valueOf(preferences[KEY_FONT_FAMILY] ?: "ROBOTO")
- } catch (e: Exception) {
- FontFamilyPreset.ROBOTO
- }
- }
-
- override val customFontUri: Flow = context.dataStore.data.map { preferences ->
- preferences[KEY_CUSTOM_FONT_URI]
- }
-
- override val bubbleStyle: Flow = context.dataStore.data.map { preferences ->
- try {
- BubbleStyle.valueOf(preferences[KEY_BUBBLE_STYLE] ?: "ROUNDED")
- } catch (e: Exception) {
- BubbleStyle.ROUNDED
- }
- }
-
- override val visualDensity: Flow = context.dataStore.data.map { preferences ->
- preferences[KEY_VISUAL_DENSITY] ?: 1.0f
- }
-
override val seedColor: Flow = context.dataStore.data.map { preferences ->
- preferences[KEY_SEED_COLOR] ?: 0xFF006D68.toInt() // Default teal color
+ preferences[KEY_SEED_COLOR] ?: 0xFF006D68.toInt()
}
- // BLE Transport Settings Flows
override val bleEnabled: Flow = context.dataStore.data.map { preferences ->
- preferences[KEY_BLE_ENABLED] ?: false // Opt-in by default (battery saving)
+ preferences[KEY_BLE_ENABLED] ?: false
}
override val transportMode: Flow = context.dataStore.data.map { preferences ->
@@ -155,14 +89,12 @@ class SettingsRepository(private val context: Context) : ISettingsRepository {
}
}
- // Onboarding Flow
override val hasCompletedOnboarding: Flow = context.dataStore.data.map { preferences ->
preferences[KEY_ONBOARDING_COMPLETED] ?: false
}
- // ✅ New Settings Flows
override val appLanguage: Flow = context.dataStore.data.map { preferences ->
- preferences[KEY_APP_LANGUAGE] ?: "en" // Default English
+ preferences[KEY_APP_LANGUAGE] ?: "en"
}
override val fontSizeScale: Flow = context.dataStore.data.map { preferences ->
@@ -240,62 +172,12 @@ class SettingsRepository(private val context: Context) : ISettingsRepository {
}
}
- // MD3E Setting Mutators
- override suspend fun setShapeStyle(style: ShapeStyle) {
- safeEdit { it[KEY_SHAPE_STYLE] = style.name }.onFailure { e ->
- Logger.e("SettingsRepository -> Failed to set shape style", e)
- }
- }
-
- override suspend fun setMotionPreset(preset: MotionPreset) {
- safeEdit { it[KEY_MOTION_PRESET] = preset.name }.onFailure { e ->
- Logger.e("SettingsRepository -> Failed to set motion preset", e)
- }
- }
-
- override suspend fun setMotionScale(scale: Float) {
- safeEdit { it[KEY_MOTION_SCALE] = scale.coerceIn(0.5f, 2.0f) }.onFailure { e ->
- Logger.e("SettingsRepository -> Failed to set motion scale", e)
- }
- }
-
- override suspend fun setFontFamilyPreset(family: FontFamilyPreset) {
- safeEdit { it[KEY_FONT_FAMILY] = family.name }.onFailure { e ->
- Logger.e("SettingsRepository -> Failed to set font family", e)
- }
- }
-
- override suspend fun setCustomFontUri(uri: String?) {
- if (uri != null) {
- safeEdit { it[KEY_CUSTOM_FONT_URI] = uri }.onFailure { e ->
- Logger.e("SettingsRepository -> Failed to set custom font", e)
- }
- } else {
- safeEdit { it.remove(KEY_CUSTOM_FONT_URI) }.onFailure { e ->
- Logger.e("SettingsRepository -> Failed to clear custom font", e)
- }
- }
- }
-
- override suspend fun setBubbleStyle(style: BubbleStyle) {
- safeEdit { it[KEY_BUBBLE_STYLE] = style.name }.onFailure { e ->
- Logger.e("SettingsRepository -> Failed to set bubble style", e)
- }
- }
-
- override suspend fun setVisualDensity(density: Float) {
- safeEdit { it[KEY_VISUAL_DENSITY] = density.coerceIn(0.8f, 1.5f) }.onFailure { e ->
- Logger.e("SettingsRepository -> Failed to set visual density", e)
- }
- }
-
override suspend fun setSeedColor(color: Int) {
safeEdit { it[KEY_SEED_COLOR] = color }.onFailure { e ->
Logger.e("SettingsRepository -> Failed to set seed color", e)
}
}
- // BLE Transport Settings Mutators
override suspend fun setBleEnabled(enabled: Boolean) {
safeEdit { it[KEY_BLE_ENABLED] = enabled }.onFailure { e ->
Logger.e("SettingsRepository -> Failed to set BLE enabled", e)
@@ -308,7 +190,6 @@ class SettingsRepository(private val context: Context) : ISettingsRepository {
}
}
- // Onboarding Mutators
override suspend fun setOnboardingCompleted() {
safeEdit { it[KEY_ONBOARDING_COMPLETED] = true }.onFailure { e ->
Logger.e("SettingsRepository -> Failed to set onboarding completed", e)
@@ -321,7 +202,6 @@ class SettingsRepository(private val context: Context) : ISettingsRepository {
}
}
- // ✅ New Settings Mutators
override suspend fun setAppLanguage(language: String) {
safeEdit { it[KEY_APP_LANGUAGE] = language }.onFailure { e ->
Logger.e("SettingsRepository -> Failed to set app language", e)
@@ -354,7 +234,6 @@ class SettingsRepository(private val context: Context) : ISettingsRepository {
override suspend fun clearCache() {
try {
- // Only clear avatar files that are not the current avatar
val currentAvatarHash = context.dataStore.data.map { it[KEY_AVATAR_HASH] }.firstOrNull()
val avatarsDir = java.io.File(context.filesDir, "avatars")
if (avatarsDir.exists() && avatarsDir.isDirectory) {
@@ -364,13 +243,10 @@ class SettingsRepository(private val context: Context) : ISettingsRepository {
}
}
}
-
- // Clear Coil image cache directory only
val coilCacheDir = java.io.File(context.cacheDir, "image_manager_disk_cache")
if (coilCacheDir.exists()) {
coilCacheDir.deleteRecursively()
}
-
Logger.d("SettingsRepository -> Cache cleared successfully")
} catch (e: Exception) {
Logger.e("SettingsRepository -> Failed to clear cache", e)
@@ -380,94 +256,32 @@ class SettingsRepository(private val context: Context) : ISettingsRepository {
override suspend fun exportBackup(): Result {
return try {
- // ✅ CODE-03: Added error handling for blocking .first() call
- // Prevents app freeze if DataStore is corrupted or locked
val prefs = try {
context.dataStore.data.first()
} catch (e: Exception) {
- Logger.e("SettingsRepository -> Failed to read DataStore", e)
return Result.failure(Exception("Failed to read preferences: ${e.message}", e))
}
-
+
val backupData = mapOf(
- // Core settings
"display_name" to prefs[KEY_DISPLAY_NAME],
"theme_mode" to prefs[KEY_THEME_MODE],
"dynamic_color" to prefs[KEY_DYNAMIC_COLOR],
"haptic_feedback" to prefs[KEY_HAPTIC_FEEDBACK],
"network_visible" to prefs[KEY_NETWORK_VISIBLE],
"avatar_hash" to prefs[KEY_AVATAR_HASH],
- // MD3E settings
- "shape_style" to prefs[KEY_SHAPE_STYLE],
- "motion_preset" to prefs[KEY_MOTION_PRESET],
- "motion_scale" to prefs[KEY_MOTION_SCALE]?.toString(),
- "font_family" to prefs[KEY_FONT_FAMILY],
- "custom_font_uri" to prefs[KEY_CUSTOM_FONT_URI],
- "bubble_style" to prefs[KEY_BUBBLE_STYLE],
- "visual_density" to prefs[KEY_VISUAL_DENSITY]?.toString(),
"seed_color" to prefs[KEY_SEED_COLOR]?.toString(),
- // App settings
"app_language" to prefs[KEY_APP_LANGUAGE],
"font_size_scale" to prefs[KEY_FONT_SIZE_SCALE]?.toString(),
"notifications_enabled" to prefs[KEY_NOTIFICATIONS_ENABLED],
"notification_sound" to prefs[KEY_NOTIFICATION_SOUND],
"notification_vibrate" to prefs[KEY_NOTIFICATION_VIBRATE],
- // BLE settings
"ble_enabled" to prefs[KEY_BLE_ENABLED],
"transport_mode" to prefs[KEY_TRANSPORT_MODE],
- // Metadata
"export_timestamp" to System.currentTimeMillis().toString()
- )
- // filterValues removes all nulls, so mapValues safely receives non-null values
- .filterValues { it != null }.mapValues { it.value.toString() }
+ ).filterValues { it != null }.mapValues { it.value.toString() }
val json = Json.encodeToString(backupData)
- Logger.d("SettingsRepository -> Backup exported successfully")
Result.success(json)
} catch (e: Exception) {
- Logger.e("SettingsRepository -> Failed to export backup", e)
- Result.failure(e)
- }
- }
-
- override suspend fun importBackup(backupJson: String): Result {
- return try {
- val backupData = Json.decodeFromString