# Deployment Instructions
-The Android app runs independently of an internet connection. The APK needs to be installed in each phone, then the phones are able to connect to each other from within the app.
+
+The Android app runs independently of an internet connection. The APK needs to be installed in each phone, then the phones are able to connect to each other from within the app.
+
1. Open the app
2. When prompted, allow location and nearby devices permissions
3. Wait for a QR code to appear on screen
4. On each phone, use the ‘Scan QR code’ button to scan the codes of adjacent devices
5. Messages are able to be sent to connected devices with the ‘Send’ button and ‘Message’ text box
-
Credential info: N/A
GitHub URL: https://github.com/grey-box/Project-Mesh
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 967417e76..61e4dba9e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -4,6 +4,8 @@ plugins {
id("kotlin-kapt")
id("com.google.devtools.ksp") version "1.9.0-1.0.13"
kotlin("plugin.serialization") version "1.9.0"
+ id("org.jetbrains.kotlinx.kover") version "0.9.3"
+ id("org.jetbrains.dokka") version "2.2.0-Beta"
}
android {
@@ -39,6 +41,10 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
+ lint {
+ // affects gradle linter
+ disable.add("UnusedResources")
+ }
buildFeatures {
compose = true
}
@@ -60,89 +66,89 @@ android {
}
dependencies {
- implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.lifecycle.runtime.ktx)
+ // ===============================
+ // General
+ // ===============================
+ implementation(libs.accompanist.permissions)
+ implementation(libs.acra.dialog)
+ implementation(libs.acra.http)
implementation(libs.androidx.activity.compose)
- implementation("ch.acra:acra-http:5.11.0")
- implementation("ch.acra:acra-dialog:5.11.0")
- implementation(platform(libs.androidx.compose.bom))
- implementation("androidx.compose.material3:material3:1.2.1")
- implementation("androidx.compose.material:material-icons-core:1.6.8")
- implementation("androidx.compose.material:material-icons-extended-android:1.6.8")
- implementation(libs.androidx.foundation)
- implementation(libs.androidx.ui)
- implementation(libs.androidx.ui.graphics)
- implementation(libs.androidx.ui.tooling.preview)
- implementation(libs.androidx.material3)
+ implementation(libs.androidx.activity)
+ implementation(libs.androidx.appcompat)
implementation(libs.androidx.constraintlayout)
- implementation(libs.material)
+ implementation(libs.androidx.core.ktx)
implementation(libs.androidx.datastore.core.v111)
+ implementation(libs.androidx.datastore.preferences.core)
implementation(libs.androidx.datastore.preferences)
+ implementation(libs.androidx.foundation)
implementation(libs.androidx.lifecycle.runtime.compose)
- implementation(libs.androidx.datastore.preferences.core)
- implementation(libs.androidx.activity)
- implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.android)
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.androidx.material.icons.core)
+ implementation(libs.androidx.material.icons.extended)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.ui)
+ implementation(libs.coil.compose)
+ implementation(libs.compose.qrpainter)
+ implementation(libs.gson) // for crash screen
+ implementation(libs.ipaddress)
+ implementation(libs.jetbrains.kotlinx.serialization.json) // For JSON serialization
+ implementation(libs.material)
+ implementation(libs.meshrabiya)
+ implementation(libs.nanohttp)
+ implementation(libs.okhttp)
+ implementation(libs.zxing.android.embedded)
+ implementation(platform(libs.androidx.compose.bom))
+
+
+ // ===============================
+ // Unit testing (JVM) deps added
+ // ===============================
+ testImplementation(libs.androidx.test.core)
+ testImplementation(libs.jetbrains.kotlinx.coroutines.test)
testImplementation(libs.junit)
- androidTestImplementation(libs.androidx.junit)
+ testImplementation(libs.mockk)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.turbine)
+
androidTestImplementation(libs.androidx.espresso.core)
- androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.ui.test.junit4)
- debugImplementation(libs.androidx.ui.tooling)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+
debugImplementation(libs.androidx.ui.test.manifest)
- implementation("com.github.UstadMobile.Meshrabiya:lib-meshrabiya:0.1d10-snapshot")
- implementation("com.github.seancfoley:ipaddress:5.3.3")
- implementation("com.squareup.okhttp3:okhttp:4.10.0")
- implementation("org.nanohttpd:nanohttpd:2.3.1")
- implementation (libs.material)
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
- implementation("com.github.yveskalume:compose-qrpainter:0.0.1")
- implementation("com.journeyapps:zxing-android-embedded:4.3.0")
- implementation(libs.androidx.appcompat)
- implementation ("io.coil-kt:coil-compose:1.4.0")
- implementation("androidx.compose.material3:material3:1.2.1")
- implementation("androidx.navigation:navigation-compose:2.7.7")
- implementation("com.google.accompanist:accompanist-permissions:0.31.1-alpha")
- // Core Kodein DI dependency
+ debugImplementation(libs.androidx.ui.tooling)
+
+ // ===============================
+ // Kodein
+ // ===============================
// For Android-specific features
- implementation ("org.kodein.di:kodein-di-framework-android-x:7.20.2")
+ implementation (libs.kodein.di.framework.android.x)
// For Jetpack Compose support
- implementation ("org.kodein.di:kodein-di-framework-compose:7.20.2")
+ implementation (libs.kodein.di.framework.compose)
- val room_version = "2.6.1"
- implementation("androidx.room:room-runtime:$room_version")
- annotationProcessor("androidx.room:room-compiler:$room_version")
+ // ===============================
+ // Room
+ // ===============================
+ annotationProcessor(libs.androidx.room.compiler)
+ implementation(libs.androidx.room.runtime)
// To use Kotlin annotation processing tool (kapt)
// kapt("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbol Processing (KSP)
- ksp("androidx.room:room-compiler:$room_version")
-
- // optional - Kotlin Extensions and Coroutines support for Room
- implementation("androidx.room:room-ktx:$room_version")
-
- // optional - RxJava2 support for Room
- implementation("androidx.room:room-rxjava2:$room_version")
-
- // optional - RxJava3 support for Room
- implementation("androidx.room:room-rxjava3:$room_version")
-
- // optional - Guava support for Room, including Optional and ListenableFuture
- implementation("androidx.room:room-guava:$room_version")
-
- // optional - Test helpers
- testImplementation("androidx.room:room-testing:$room_version")
-
- // optional - Paging 3 Integration
- implementation("androidx.room:room-paging:$room_version")
-
- // for crash scren
- implementation("com.google.code.gson:gson:2.10.1")
-
- // For JSON serialisation
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
-}
\ No newline at end of file
+ ksp(libs.androidx.room.compiler)
+
+ implementation(libs.androidx.room.guava) // optional - Guava support for Room, including Optional and ListenableFuture
+ implementation(libs.androidx.room.ktx) // optional - Kotlin Extensions and Coroutines support for Room
+ implementation(libs.androidx.room.paging) // optional - Paging 3 Integration
+ implementation(libs.androidx.room.rxjava2) // optional - RxJava2 support for Room
+ implementation(libs.androidx.room.rxjava3) // optional - RxJava3 support for Room
+ testImplementation(libs.androidx.room.testing) // optional - Test helpers
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a5ed25801..8039c6669 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,8 @@
-
-
+
+
@@ -24,24 +26,27 @@
+ android:name="android.hardware.camera"
+ android:required="false"
+ />
-
-
+ android:name="android.permission.NEARBY_WIFI_DEVICES"
+ android:usesPermissionFlags="neverForLocation"
+ />
+
+
-
+
-
+
@@ -55,26 +60,28 @@
+ android:name=".GlobalApp"
+ android:allowBackup="true"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:usesCleartextTraffic="true"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat"
+ android:hardwareAccelerated="true"
+ tools:targetApi="31"
+ >
+ android:name=".MainActivity"
+ android:exported="true"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.ProjectMesh.Launcher"
+ >
@@ -82,21 +89,28 @@
+ android:name="androidx.core.content.FileProvider"
+ android:authorities="com.greybox.projectmesh.fileprovider"
+ android:grantUriPermissions="true"
+ android:exported="false"
+ >
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/filepaths"
+ />
-
+ android:name="com.journeyapps.barcodescanner.CaptureActivity"
+ android:screenOrientation="portrait"
+ android:stateNotNeeded="true"
+ tools:replace="android:screenOrientation"
+ />
+
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt b/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt
index 7f22e1494..0156a677c 100644
--- a/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt
+++ b/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt
@@ -5,7 +5,6 @@ import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
-import android.os.Environment
import android.util.Log
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
@@ -19,11 +18,9 @@ import com.greybox.projectmesh.extension.networkDataStore
import com.greybox.projectmesh.server.AppServer
import com.ustadmobile.meshrabiya.ext.addressToDotNotation
import com.ustadmobile.meshrabiya.ext.asInetAddress
-import com.ustadmobile.meshrabiya.ext.requireAddressAsInt
import com.ustadmobile.meshrabiya.vnet.AndroidVirtualNode
import com.ustadmobile.meshrabiya.vnet.randomApipaAddr
import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -38,8 +35,6 @@ import org.kodein.di.singleton
import java.io.File
import java.net.InetAddress
import java.time.Duration
-import java.util.UUID
-import java.util.concurrent.ConcurrentHashMap
import com.greybox.projectmesh.user.UserRepository
import com.greybox.projectmesh.messaging.data.entities.Message
diff --git a/app/src/main/java/com/greybox/projectmesh/MainActivity.kt b/app/src/main/java/com/greybox/projectmesh/MainActivity.kt
index 468680675..2f464032c 100644
--- a/app/src/main/java/com/greybox/projectmesh/MainActivity.kt
+++ b/app/src/main/java/com/greybox/projectmesh/MainActivity.kt
@@ -1,95 +1,76 @@
package com.greybox.projectmesh
-import android.annotation.SuppressLint
-import android.app.AlertDialog
-import android.content.Context
-import android.content.Context.MODE_PRIVATE
-import android.content.Intent
import android.content.SharedPreferences
-import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
-import android.os.PowerManager
-import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
+import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
+import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.greybox.projectmesh.debug.CrashHandler
import com.greybox.projectmesh.debug.CrashScreenActivity
-import com.greybox.projectmesh.navigation.BottomNavItem
+import com.greybox.projectmesh.messaging.data.entities.Conversation
+import com.greybox.projectmesh.messaging.ui.screens.ChatScreen
+import com.greybox.projectmesh.messaging.ui.screens.ConversationsHomeScreen
+import com.greybox.projectmesh.messaging.ui.viewmodels.ChatScreenViewModel
import com.greybox.projectmesh.navigation.BottomNavigationBar
+import com.greybox.projectmesh.navigation.BottomNavItem
import com.greybox.projectmesh.server.AppServer
+import com.greybox.projectmesh.testing.TestDeviceService
import com.greybox.projectmesh.ui.theme.AppTheme
import com.greybox.projectmesh.ui.theme.ProjectMeshTheme
+import com.greybox.projectmesh.user.UserRepository
import com.greybox.projectmesh.viewModel.SharedUriViewModel
-import com.greybox.projectmesh.messaging.ui.screens.ChatScreen
import com.greybox.projectmesh.views.HomeScreen
-import com.greybox.projectmesh.views.SettingsScreen
+import com.greybox.projectmesh.views.LogScreen
import com.greybox.projectmesh.views.NetworkScreen
+import com.greybox.projectmesh.views.OnboardingScreen
import com.greybox.projectmesh.views.PingScreen
import com.greybox.projectmesh.views.ReceiveScreen
import com.greybox.projectmesh.views.SelectDestNodeScreen
import com.greybox.projectmesh.views.SendScreen
-import com.greybox.projectmesh.views.OnboardingScreen
-import com.greybox.projectmesh.testing.TestDeviceService
-import org.kodein.di.DI
-import org.kodein.di.DIAware
-import org.kodein.di.android.closestDI
-import org.kodein.di.compose.withDI
-import org.kodein.di.instance
+import com.greybox.projectmesh.views.SettingsScreen
import java.io.File
-import java.util.Locale
import java.net.InetAddress
-import com.greybox.projectmesh.messaging.ui.screens.ChatNodeListScreen
-import com.greybox.projectmesh.messaging.ui.screens.ConversationsHomeScreen
-import com.greybox.projectmesh.user.UserRepository
+import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.remember
-import kotlinx.coroutines.launch
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
-import androidx.lifecycle.lifecycleScope
-import kotlinx.coroutines.launch
-import com.greybox.projectmesh.messaging.data.entities.Conversation
-import com.greybox.projectmesh.messaging.ui.viewmodels.ChatScreenViewModel
-import com.greybox.projectmesh.views.LogScreen
+import org.kodein.di.android.closestDI
+import org.kodein.di.compose.withDI
+import org.kodein.di.DI
+import org.kodein.di.DIAware
+import org.kodein.di.instance
import com.greybox.projectmesh.views.RequestPermissionsScreen
@@ -129,7 +110,7 @@ class MainActivity : ComponentActivity(), DIAware {
mutableStateOf(settingPref.getString(
"language", "en") ?: "en")
}
- var restartServerKey by remember {mutableStateOf(0)}
+ var restartServerKey by remember {mutableIntStateOf(0)}
var deviceName by remember {
mutableStateOf(settingPref.getString("device_name", Build.MODEL) ?: Build.MODEL)
}
diff --git a/app/src/main/java/com/greybox/projectmesh/components/WifiConnection.kt b/app/src/main/java/com/greybox/projectmesh/components/WifiConnection.kt
index 864274d4d..a40fc29d4 100644
--- a/app/src/main/java/com/greybox/projectmesh/components/WifiConnection.kt
+++ b/app/src/main/java/com/greybox/projectmesh/components/WifiConnection.kt
@@ -28,7 +28,6 @@ import com.ustadmobile.meshrabiya.vnet.wifi.WifiConnectException
import java.util.regex.Pattern
import android.util.Log
-
// This File is to pre-check the wifi connection, reusing from Meshrabiya test app
/*
WorkFlow:
@@ -41,24 +40,62 @@ WorkFlow:
4. It handles the result (successful connection or error) and updates the UI as needed.
*/
-fun interface ConnectWifiLauncher{
+/**
+ * Functional interface representing a launcher for Wi-Fi connections.
+ */
+fun interface ConnectWifiLauncher {
+ /**
+ * Launch a connection attempt with the specified Wi-Fi configuration.
+ *
+ * @param config The Wi-Fi configuration to connect to.
+ */
fun launch(config: WifiConnectConfig)
}
+/**
+ * Represents a request to connect to a Wi-Fi network.
+ *
+ * @property receivedTime The timestamp when the request was created.
+ * @property connectConfig The configuration of the Wi-Fi network to connect to.
+ */
data class ConnectRequest(
val receivedTime: Long = 0,
val connectConfig: WifiConnectConfig,
)
+/**
+ * Result of a Wi-Fi connection attempt.
+ *
+ * @property hotspotConfig The configuration of the hotspot connected to, or null if failed.
+ * @property exception Any exception that occurred during connection, or null if successful.
+ * @property isWifiConnected True if the connection was successful, false otherwise.
+ */
data class ConnectWifiLauncherResult(
val hotspotConfig: WifiConnectConfig?,
val exception: Exception? = null,
val isWifiConnected: Boolean = false,
)
+
+/**
+ * Status of the ConnectWifiLauncher during a Wi-Fi connection attempt.
+ */
enum class ConnectWifiLauncherStatus {
INACTIVE, REQUESTING_PERMISSION, LOOKING_FOR_NETWORK, REQUESTING_LINK,
}
+/**
+ * Composable function that provides a [ConnectWifiLauncher] for managing Wi-Fi connections.
+ *
+ * It handles permission requests, network association via [CompanionDeviceManager],
+ * and provides status updates and connection results.
+ *
+ * @param node The [AndroidVirtualNode] representing the local virtual node.
+ * @param logger Optional logger for debugging messages.
+ * @param onStatusChange Optional callback invoked when the launcher status changes.
+ * @param onResult Callback invoked with the result of the Wi-Fi connection attempt.
+ *
+ * @return A [ConnectWifiLauncher] that can be used to initiate Wi-Fi connections.
+ */
@Composable
fun meshrabiyaConnectLauncher(
node: AndroidVirtualNode,
@@ -236,4 +273,4 @@ fun meshrabiyaConnectLauncher(
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/db/MeshDatabase.kt b/app/src/main/java/com/greybox/projectmesh/db/MeshDatabase.kt
index 1f3dba4cc..05dfebed9 100644
--- a/app/src/main/java/com/greybox/projectmesh/db/MeshDatabase.kt
+++ b/app/src/main/java/com/greybox/projectmesh/db/MeshDatabase.kt
@@ -9,6 +9,12 @@ import com.greybox.projectmesh.messaging.data.entities.Conversation
import com.greybox.projectmesh.user.UserDao
import com.greybox.projectmesh.user.UserEntity
+/**
+ * Room database for the ProjectMesh application.
+ *
+ * This database stores messages, conversations, and user entities.
+ * It provides DAOs to access and manipulate each type of data.
+ */
@Database(
entities = [
Message::class,
@@ -19,7 +25,25 @@ import com.greybox.projectmesh.user.UserEntity
exportSchema = false
)
abstract class MeshDatabase : RoomDatabase() {
+
+ /**
+ * Provides access to message-related database operations.
+ *
+ * @return A [MessageDao] instance for querying and modifying messages.
+ */
abstract fun messageDao(): MessageDao
+
+ /**
+ * Provides access to user-related database operations.
+ *
+ * @return A [UserDao] instance for querying and modifying user entities.
+ */
abstract fun userDao(): UserDao
+
+ /**
+ * Provides access to conversation-related database operations.
+ *
+ * @return A [ConversationDao] instance for querying and modifying conversations.
+ */
abstract fun conversationDao(): ConversationDao
}
diff --git a/app/src/main/java/com/greybox/projectmesh/debug/CrashHandler.kt b/app/src/main/java/com/greybox/projectmesh/debug/CrashHandler.kt
index 3cc15d6a9..5c7cf28d7 100644
--- a/app/src/main/java/com/greybox/projectmesh/debug/CrashHandler.kt
+++ b/app/src/main/java/com/greybox/projectmesh/debug/CrashHandler.kt
@@ -12,24 +12,48 @@ import java.lang.Exception
import java.lang.Thread.UncaughtExceptionHandler
import kotlin.system.exitProcess
+/**
+ * Custom [Thread.UncaughtExceptionHandler] to handle uncaught exceptions in the app.
+ *
+ * This handler launches a specified activity when a crash occurs, passing the exception
+ * details via an Intent, and then terminates the app.
+ *
+ * @param context The application context used to launch the crash activity.
+ * @param defaultHandler The default uncaught exception handler to fallback on.
+ * @param activityToBeLaunched The activity class to be launched when a crash occurs.
+ */
+class CrashHandler(
+ private val context: Context,
+ private val defaultHandler: UncaughtExceptionHandler,
+ private val activityToBeLaunched: Class<*>
+) : Thread.UncaughtExceptionHandler {
-class CrashHandler(private val context: Context, private val defaultHandler: UncaughtExceptionHandler, private val activityToBeLaunched: Class<*>) : Thread.UncaughtExceptionHandler {
-
+ /**
+ * Handles uncaught exceptions thrown by any thread.
+ *
+ * @param thread The thread where the exception occurred.
+ * @param throwable The uncaught exception.
+ */
override fun uncaughtException(thread: Thread, throwable: Throwable) {
try {
- launchActivity(context,activityToBeLaunched,throwable)
+ launchActivity(context, activityToBeLaunched, throwable)
exitProcess(status = 1)
- } catch (e: Exception)
- {
- defaultHandler.uncaughtException(thread,throwable)
+ } catch (e: Exception) {
+ defaultHandler.uncaughtException(thread, throwable)
}
}
- private fun launchActivity(applicationContext: Context, activity: Class<*>, exception: Throwable)
- {
+ /**
+ * Launches the crash reporting activity with the exception details.
+ *
+ * @param applicationContext The context used to start the activity.
+ * @param activity The activity class to launch.
+ * @param exception The exception to pass to the activity.
+ */
+ private fun launchActivity(applicationContext: Context, activity: Class<*>, exception: Throwable) {
val crashIntent = Intent(applicationContext, activity).also {
it.putExtra("CrashData", Gson().toJson(exception))
- Log.e("Project Mesh Error","Error: ",exception);
+ Log.e("Project Mesh Error", "Error: ", exception)
}
crashIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -38,22 +62,34 @@ class CrashHandler(private val context: Context, private val defaultHandler: Unc
}
companion object {
- fun init(applicationContext: Context, activityToBeLaunched: Class<*>)
- {
- val handler = CrashHandler(applicationContext,Thread.getDefaultUncaughtExceptionHandler() as UncaughtExceptionHandler, activityToBeLaunched)
+ /**
+ * Initializes the [CrashHandler] and sets it as the default uncaught exception handler.
+ *
+ * @param applicationContext The application context used to create the handler.
+ * @param activityToBeLaunched The activity class to launch on crash.
+ */
+ fun init(applicationContext: Context, activityToBeLaunched: Class<*>) {
+ val handler = CrashHandler(
+ applicationContext,
+ Thread.getDefaultUncaughtExceptionHandler() as UncaughtExceptionHandler,
+ activityToBeLaunched
+ )
Thread.setDefaultUncaughtExceptionHandler(handler)
}
- fun getThrowableFromIntent(intent: Intent): Throwable?
- {
+ /**
+ * Retrieves a [Throwable] from an intent containing crash data.
+ *
+ * @param intent The intent containing serialized crash data.
+ * @return The deserialized [Throwable], or null if parsing fails.
+ */
+ fun getThrowableFromIntent(intent: Intent): Throwable? {
return try {
Gson().fromJson(intent.getStringExtra("CrashData"), Throwable::class.java)
- }
- catch (e: Exception) {
- Log.e("CrashHandler","getThrowableFromIntent: ",e);
+ } catch (e: Exception) {
+ Log.e("CrashHandler", "getThrowableFromIntent: ", e)
null
}
-
}
}
}
diff --git a/app/src/main/java/com/greybox/projectmesh/extension/ContentResolverExtension.kt b/app/src/main/java/com/greybox/projectmesh/extension/ContentResolverExtension.kt
index bf474affe..2f3c4a0da 100644
--- a/app/src/main/java/com/greybox/projectmesh/extension/ContentResolverExtension.kt
+++ b/app/src/main/java/com/greybox/projectmesh/extension/ContentResolverExtension.kt
@@ -5,6 +5,12 @@ import android.net.Uri
import android.provider.OpenableColumns
import androidx.core.net.toFile
+/**
+ * Represents the name and size of a file referenced by a [Uri].
+ *
+ * @property name The display name of the file, or null if it cannot be determined.
+ * @property size The size of the file in bytes, or -1 if unknown.
+ */
data class UriNameAndSize(
val name: String?,
val size: Long,
@@ -16,12 +22,23 @@ It will return a UriNameAndSize object that contains the name and size of the fi
Two Condition:
1. The uri is a file uri
2. The uri is a content uri
+*/
+
+/**
+ * Retrieves the name and size of a file referenced by the given [uri].
+ *
+ * Supports both "file" scheme URIs and "content" scheme URIs.
+ *
+ * @receiver The [ContentResolver] used to query content URIs.
+ * @param uri The [Uri] pointing to the file.
+ * @return A [UriNameAndSize] object containing the file's name and size, or
+ * null name and -1 size if the information cannot be determined.
*/
fun ContentResolver.getUriNameAndSize(uri: Uri): UriNameAndSize {
return if(uri.scheme == "file") {
val uriFile = uri.toFile()
UriNameAndSize(uriFile.name, uriFile.length())
- }else {
+ } else {
query(
uri, null, null, null, null
)?.use { cursor ->
@@ -35,7 +52,7 @@ fun ContentResolver.getUriNameAndSize(uri: Uri): UriNameAndSize {
cursor.getString(sizeIndex)
}
UriNameAndSize(cursor.getString(nameIndex), size?.toLong() ?: -1L)
- }else {
+ } else {
null
}
} ?: UriNameAndSize(null, -1)
diff --git a/app/src/main/java/com/greybox/projectmesh/extension/ContextExt.kt b/app/src/main/java/com/greybox/projectmesh/extension/ContextExt.kt
index c2cfa4632..beb3159c6 100644
--- a/app/src/main/java/com/greybox/projectmesh/extension/ContextExt.kt
+++ b/app/src/main/java/com/greybox/projectmesh/extension/ContextExt.kt
@@ -14,11 +14,13 @@ import com.ustadmobile.meshrabiya.MeshrabiyaConstants
/*
context is a class that provides access to application-specific resources and classes.
This File contains several context related extension functions that will use in this app.
- */
+*/
/**
- * On Android 13+ we can use the NEARBY_WIFI_DEVICES permission instead of the location permission.
- * On earlier versions, we need fine location permission
+ * The permission string required for accessing nearby Wi-Fi devices.
+ *
+ * On Android 13+ (SDK 33+), uses [Manifest.permission.NEARBY_WIFI_DEVICES].
+ * On earlier versions, falls back to [Manifest.permission.ACCESS_FINE_LOCATION].
*/
val NEARBY_WIFI_PERMISSION_NAME = if(Build.VERSION.SDK_INT >= 33){
Manifest.permission.NEARBY_WIFI_DEVICES
@@ -26,32 +28,65 @@ val NEARBY_WIFI_PERMISSION_NAME = if(Build.VERSION.SDK_INT >= 33){
Manifest.permission.ACCESS_FINE_LOCATION
}
-// check if the app has the nearby wifi devices permission
+/**
+ * Checks whether the app has permission to access nearby Wi-Fi devices (Android 13+)
+ * or fine location (pre-Android 13).
+ *
+ * @receiver The [Context] used to check permissions.
+ * @return `true` if the required permission is granted, `false` otherwise.
+ */
fun Context.hasNearbyWifiDevicesOrLocationPermission(): Boolean {
return ContextCompat.checkSelfPermission(
this, NEARBY_WIFI_PERMISSION_NAME
) == PackageManager.PERMISSION_GRANTED
}
-// check if the app has the bluetooth connect permission
+/**
+ * Checks whether the app has permission to connect to Bluetooth devices.
+ *
+ * On Android 12+ (SDK 31+), uses [Manifest.permission.BLUETOOTH_CONNECT].
+ * On earlier versions, always returns `true`.
+ *
+ * @receiver The [Context] used to check permissions.
+ * @return `true` if the permission is granted or not required, `false` otherwise.
+ */
fun Context.hasBluetoothConnectPermission(): Boolean {
return if(Build.VERSION.SDK_INT >= 31) {
ContextCompat.checkSelfPermission(
this, Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
- }else {
+ } else {
true
}
}
-// create a DataStore instance that Meshrabiya can use to remember networks
+/**
+ * Provides a [DataStore] instance named "meshr_settings" for storing persistent
+ * network-related preferences used by Meshrabiya.
+ */
val Context.networkDataStore: DataStore by preferencesDataStore(name = "meshr_settings")
-// Check if the device supports WiFi STA/AP Concurrency
+/**
+ * Checks if the device supports Wi-Fi STA/AP concurrency (simultaneous station and access point mode).
+ *
+ * Requires Android 11+ (SDK 30+).
+ *
+ * @receiver The [Context] used to access [WifiManager].
+ * @return `true` if STA/AP concurrency is supported, `false` otherwise.
+ */
fun Context.hasStaApConcurrency(): Boolean {
return Build.VERSION.SDK_INT >= 30 && getSystemService(WifiManager::class.java).isStaApConcurrencySupported
}
+/**
+ * Returns a detailed string describing the device and Wi-Fi capabilities.
+ *
+ * Includes Meshrabiya version, Android version, device manufacturer/model,
+ * 5GHz support, local-only station concurrency, STA/AP concurrency, and Wi-Fi Aware support.
+ *
+ * @receiver The [Context] used to access system services and package manager.
+ * @return A formatted [String] describing the device and Wi-Fi features.
+ */
fun Context.deviceInfo(): String {
val wifiManager = getSystemService(WifiManager::class.java)
val hasStaConcurrency = Build.VERSION.SDK_INT >= 31 &&
@@ -69,4 +104,4 @@ fun Context.deviceInfo(): String {
append("Station-AP concurrency: $hasStaApConcurrency\n")
append("WifiAware support: $hasWifiAwareSupport\n")
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/extension/ListExtension.kt b/app/src/main/java/com/greybox/projectmesh/extension/ListExtension.kt
index 598d619f5..52282b949 100644
--- a/app/src/main/java/com/greybox/projectmesh/extension/ListExtension.kt
+++ b/app/src/main/java/com/greybox/projectmesh/extension/ListExtension.kt
@@ -3,6 +3,16 @@ package com.greybox.projectmesh.extension
/*
This is an extension function on Kotlin's List class, allowing to apply an update to the
first element in a list that matches a given condition, then returning a updated list
+*/
+
+/**
+ * Returns a new list with the first element that satisfies [condition] updated by [function].
+ *
+ * If no element matches [condition], the original list is returned unchanged.
+ *
+ * @param condition A predicate to identify which element to update.
+ * @param function A transformation function applied to the matching element.
+ * @return A new [List] with the updated element, or the original list if no element matches.
*/
inline fun List.updateItem(
condition: (T) -> Boolean,
@@ -20,4 +30,4 @@ inline fun List.updateItem(
newList -> newList[index] = function(this[index])
}.toList()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/extension/NetworkUtils.kt b/app/src/main/java/com/greybox/projectmesh/extension/NetworkUtils.kt
index d9b8ae0ef..1765ea6bd 100644
--- a/app/src/main/java/com/greybox/projectmesh/extension/NetworkUtils.kt
+++ b/app/src/main/java/com/greybox/projectmesh/extension/NetworkUtils.kt
@@ -4,8 +4,14 @@ import com.ustadmobile.meshrabiya.vnet.AndroidVirtualNode
import org.kodein.di.DI
import org.kodein.di.instance
+/**
+ * Retrieves the local IP address of the [AndroidVirtualNode] from a Kodein [DI] container.
+ *
+ * @param di The [DI] instance used to obtain the [AndroidVirtualNode].
+ * @return The host IP address of the node as a [String].
+ */
fun getLocalIpFromDI(di: DI): String {
// Retrieve the AndroidVirtualNode from DI and return its IP address
val node: AndroidVirtualNode by di.instance()
return node.address.hostAddress
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/extension/WifiListItem.kt b/app/src/main/java/com/greybox/projectmesh/extension/WifiListItem.kt
index d8992dfc3..ab94ae781 100644
--- a/app/src/main/java/com/greybox/projectmesh/extension/WifiListItem.kt
+++ b/app/src/main/java/com/greybox/projectmesh/extension/WifiListItem.kt
@@ -24,8 +24,18 @@ import com.ustadmobile.meshrabiya.ext.addressToDotNotation
import com.ustadmobile.meshrabiya.vnet.VirtualNode
import kotlinx.coroutines.runBlocking
import com.greybox.projectmesh.user.UserRepository
+
+/**
+ * Displays a single Wi-Fi node in a list with device information and mesh network status.
+ *
+ * This composable shows the device icon, name (from IP address), IP in dot notation,
+ * and mesh network details including ping time and hop count.
+ *
+ * @param wifiAddress The integer IP address of the Wi-Fi node.
+ * @param wifiEntry The [VirtualNode.LastOriginatorMessage] containing the node's mesh message data.
+ * @param onClick Optional lambda invoked when the list item is clicked, providing the node's IP in dot notation.
+ */
@Composable
-// Display a single connected wifi station
fun WifiListItem(
wifiAddress: Int,
wifiEntry: VirtualNode.LastOriginatorMessage,
@@ -89,4 +99,4 @@ fun WifiListItem(
}
)
HorizontalDivider()
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/README.md b/app/src/main/java/com/greybox/projectmesh/messaging/README.md
index 120b26d98..d8f361889 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/README.md
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/README.md
@@ -1,9 +1,11 @@
# Messaging Module Documentation
## Overview
+
The messaging module is a core component of Project Mesh that enables peer-to-peer text communications between devices on the local mesh network. It provides a structured architecture for sending, receiving, storing, and displaying messages without requiring internet connectivity.
## Package Structure
+
```
messaging/
├── data/ # Data layer (entities and DAOs)
@@ -16,11 +18,14 @@ messaging/
├── screens/ # Composable UI screens
└── viewmodels/# View state management
```
-## Core Components
-### 1. Data Models
-#### Message Entity
-```Kotlin
+## Core Components
+
+### 1. Data Models
+
+#### Message Entity
+
+```Kotlin
@Serializable
@Entity(tableName = "message")
data class Message(
@@ -32,8 +37,10 @@ data class Message(
@ColumnInfo(name= "file") val file: URI? = null
)
```
+
#### Conversation Entity
-```Kotlin
+
+```Kotlin
@Entity(tableName = "conversations")
data class Conversation(
@PrimaryKey val id: String, // Composite ID of the two users
@@ -46,44 +53,63 @@ data class Conversation(
@ColumnInfo(name = "is_online") val isOnline: Boolean = false // Online status
)
```
+
### 2. Repositories
+
#### MessageRepository
+
Manages message data operations, including retrieving and storing messages.
+
#### ConversationRepository
+
Manages conversation data, including creating and updating conversations, tracking user statuses, and managing unread messages.
+
### 3. Network Components
+
#### MessageNetworkHandler
- Handles network communication for sending and receiving messages using HTTP requests.
+
+Handles network communication for sending and receiving messages using HTTP requests.
+
### 4. UI Components
+
#### ChatScreen
- Displays messages in a conversation and provides UI controls for sending new messages.
+
+Displays messages in a conversation and provides UI controls for sending new messages.
+
#### ConversationsHomeScreen
- Displays a list of all conversations with status indicators and message previews.
+
+Displays a list of all conversations with status indicators and message previews.
## Architecture and Data Flow
+
### Message Flow
#### 1. User Sends Message:
-* User enters text in ChatScreen and taps Send
-* ChatScreenViewModel processes the input
-* Message is first saved locally in the database
-* MessageNetworkHandler sends the message to recipient via HTTP
+
+- User enters text in ChatScreen and taps Send
+- ChatScreenViewModel processes the input
+- Message is first saved locally in the database
+- MessageNetworkHandler sends the message to recipient via HTTP
+
#### 2. Message Reception:
-* AppServer receives HTTP request on /chat endpoint
-* MessageNetworkHandler processes the incoming message
-* Message is stored in local database
-* ConversationRepository updates the conversation
-* UI is updated via StateFlow collection
+
+- AppServer receives HTTP request on /chat endpoint
+- MessageNetworkHandler processes the incoming message
+- Message is stored in local database
+- ConversationRepository updates the conversation
+- UI is updated via StateFlow collection
### Integration with Project Mesh Components
+
#### Network Integration
+
Messages are transmitted over the mesh network created by the Meshrabiya library. The system uses:
1. `AppServer`: Provides HTTP endpoints for receiving messages and handles file transfers.
2. `DeviceStatusManager`: Tracks online/offline status of devices to determine message deliverability.
3. `AndroidVirtualNode`: Manages the underlying mesh network connections.
-```kotlin
+```kotlin
// In AppServer.kt
// Handles incoming chat messages
else if(path.startsWith("/chat")) {
@@ -91,24 +117,26 @@ else if(path.startsWith("/chat")) {
val chatMessage = deserialzedJSON.content
val time = deserialzedJSON.dateReceived
val senderIp = deserialzedJSON.sender
-
+
// Handle message via MessageNetworkHandler
val message = MessageNetworkHandler.handleIncomingMessage(
chatMessage, time, senderIp, incomingfile
)
-
+
// Save to database
db.messageDao().addMessage(message)
}
```
-#### User System Integration
+#### User System Integration
+
Messages and conversations are linked to user profiles:
+
1. Each message contains a sender field with the username
2. Conversations use a composite ID created from the UUIDs of both participants
3. Online status is synchronized with the DeviceStatusManager
-```kotlin
+```kotlin
// In ConversationUtils.kt
fun createConversationId(uuid1: String, uuid2: String): String {
// Sort UUIDs to ensure consistent IDs regardless of sender/receiver
@@ -117,15 +145,21 @@ fun createConversationId(uuid1: String, uuid2: String): String {
```
#### Database Integration
+
The messaging module uses Room database for persistence:
+
1. **MeshDatabase**: Central database that contains tables for:
-* `messages`: Stores all message content
-* `conversations`: Stores conversation metadata
-* `users`: Stores user profile information
+
+- `messages`: Stores all message content
+- `conversations`: Stores conversation metadata
+- `users`: Stores user profile information
+
2. Relationship Flow:
-* Users have multiple Conversations
-* Conversations contain multiple Messages
-* Messages reference their Conversation via the chat field
+
+- Users have multiple Conversations
+- Conversations contain multiple Messages
+- Messages reference their Conversation via the chat field
+
```kotlin
// In MeshDatabase.kt
@Database(
@@ -143,35 +177,43 @@ abstract class MeshDatabase : RoomDatabase() {
abstract fun conversationDao(): ConversationDao
}
```
+
## Special Features
+
### Offline Messaging
+
1. Messages are always stored locally first
2. If recipient is offline, message remains in local database
3. UI indicates delivery status based on device connectivity
4. Messages appear in conversation history regardless of delivery status
+
### Test Device Integration
+
Special handling for test devices that simulate real users:
-* Online test device automatically responds with echo messages
-* Offline test device stores messages locally but never receives them
+
+- Online test device automatically responds with echo messages
+- Offline test device stores messages locally but never receives them
### File Attachments
+
1. Messages can include file URI attachments
2. Files are transferred separately using the file transfer system
3. Messages with attachments display file indicators in the UI
-### Usage Example
-#### Conversations Screen:
+### Usage Example
+
+#### Conversations Screen:
-* Online and Offline Users Appear With Appropriate Read Receipts from Built-in Sample Messages
-* Connected device appears in the Conversation Screen
+- Online and Offline Users Appear With Appropriate Read Receipts from Built-in Sample Messages
+- Connected device appears in the Conversation Screen
-#### Chat Screen Initial Impressions
+#### Chat Screen Initial Impressions
-* When chatting for the first time a prompt appears to start a chat
+- When chatting for the first time a prompt appears to start a chat
#### Sending A Message
@@ -184,7 +226,7 @@ Special handling for test devices that simulate real users:
fun sendChatMessage(virtualAddress: InetAddress, message: String, file: URI?) {
val sendTime = System.currentTimeMillis()
val isOnline = DeviceStatusManager.isDeviceOnline(ipAddress)
-
+
// Create message entity
val messageEntity = Message(
id = 0,
@@ -194,17 +236,17 @@ fun sendChatMessage(virtualAddress: InetAddress, message: String, file: URI?) {
chat = chatName,
file = file
)
-
+
viewModelScope.launch {
// Save to local database
db.messageDao().addMessage(messageEntity)
-
+
// Update conversation
conversationRepository.updateWithMessage(
conversationId = conversation.id,
message = messageEntity
)
-
+
// Send message if recipient is online
if (isOnline) {
appServer.sendChatMessageWithStatus(
@@ -214,11 +256,12 @@ fun sendChatMessage(virtualAddress: InetAddress, message: String, file: URI?) {
}
}
```
-#### Receiving and Displaying Messages
+
+#### Receiving and Displaying Messages
**Example**: Bob Recieves Message From Alice
-1. Conversation is Updated and Read Receipt is shown:
+1. Conversation is Updated and Read Receipt is shown:
@@ -226,7 +269,7 @@ fun sendChatMessage(virtualAddress: InetAddress, message: String, file: URI?) {
-3. When Going back to the Chat Screen Read Status is Updated:
+3. When Going back to the Chat Screen Read Status is Updated:
@@ -243,45 +286,57 @@ init {
}
}
```
+
## Best Practices
+
### 1. Consider Network Conditions:
-* Always check device online status before sending
-* Provide clear UI feedback for undelivered messages
-* Handle intermittent connectivity gracefully
+
+- Always check device online status before sending
+- Provide clear UI feedback for undelivered messages
+- Handle intermittent connectivity gracefully
### 2. Database Operations:
-* Perform all database operations on IO dispatchers
-* Use Room's Flow API for reactive UI updates
-* Keep transactions atomic to prevent data corruption
+
+- Perform all database operations on IO dispatchers
+- Use Room's Flow API for reactive UI updates
+- Keep transactions atomic to prevent data corruption
### 3. User Experience:
-* Show clear online/offline indicators
-* Provide delivery status for messages
-* Update conversation timestamps and previews promptly
+
+- Show clear online/offline indicators
+- Provide delivery status for messages
+- Update conversation timestamps and previews promptly
### 4. Security Considerations:
-* Validate message content before processing
-* Use proper JSON schema validation for incoming messages
-* Sanitize user input to prevent injection attacks
+
+- Validate message content before processing
+- Use proper JSON schema validation for incoming messages
+- Sanitize user input to prevent injection attacks
## Troubleshooting
+
### Common Issues
+
#### 1. Messages Not Sending:
-* Check device status in DeviceStatusManager
-* Verify network connectivity between devices
-* Confirm AppServer is running on both devices
+
+- Check device status in DeviceStatusManager
+- Verify network connectivity between devices
+- Confirm AppServer is running on both devices
#### 2. Missing Conversations:
-* Ensure user profile exchange was successful
-* Check conversation ID generation is consistent
-* Verify database migrations have completed
+
+- Ensure user profile exchange was successful
+- Check conversation ID generation is consistent
+- Verify database migrations have completed
#### 3. UI Not Updating:
-* Confirm StateFlow collection is active
-* Check database queries are properly observed
-* Verify Composable recomposition triggers
+
+- Confirm StateFlow collection is active
+- Check database queries are properly observed
+- Verify Composable recomposition triggers
## Future Enhancements
+
1. **Message Encryption**: Add end-to-end encryption for message content
2. **Message Status**: Add read receipts and delivery confirmations
3. **Rich Media**: Enhance support for images, videos, and other media types
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/data/dao/ConversationDao.kt b/app/src/main/java/com/greybox/projectmesh/messaging/data/dao/ConversationDao.kt
index 8e1439369..5616eedd6 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/data/dao/ConversationDao.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/data/dao/ConversationDao.kt
@@ -8,32 +8,91 @@ import androidx.room.Update
import com.greybox.projectmesh.messaging.data.entities.Conversation
import kotlinx.coroutines.flow.Flow
+/**
+ * Data Access Object for [Conversation] entities.
+ *
+ * Provides methods to query, insert, and update conversations in the Room database.
+ */
@Dao
interface ConversationDao {
+
+ /**
+ * Returns a flow of all conversations, sorted by the timestamp of the last message in descending order.
+ *
+ * @return [Flow] emitting a list of [Conversation] objects whenever the data changes.
+ */
@Query("SELECT * FROM conversations ORDER BY last_message_time DESC")
fun getAllConversationsFlow(): Flow>
+ /**
+ * Retrieves a conversation by its unique ID.
+ *
+ * @param conversationId The unique ID of the conversation.
+ * @return The [Conversation] if found, or `null` if no matching conversation exists.
+ */
@Query("SELECT * FROM conversations WHERE id = :conversationId LIMIT 1")
suspend fun getConversationById(conversationId: String): Conversation?
+ /**
+ * Retrieves a conversation associated with a specific user UUID.
+ *
+ * @param userUuid The UUID of the user.
+ * @return The [Conversation] if found, or `null` if no matching conversation exists.
+ */
@Query("SELECT * FROM conversations WHERE user_uuid = :userUuid LIMIT 1")
suspend fun getConversationByUserUuid(userUuid: String): Conversation?
+ /**
+ * Inserts a new conversation into the database.
+ *
+ * If a conversation with the same ID already exists, it will be replaced.
+ *
+ * @param conversation The [Conversation] to insert.
+ */
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertConversation(conversation: Conversation)
+ /**
+ * Updates an existing conversation in the database.
+ *
+ * @param conversation The [Conversation] to update.
+ */
@Update
suspend fun updateConversation(conversation: Conversation)
+ /**
+ * Updates the online status and user address for a conversation based on the user UUID.
+ *
+ * @param userUuid The UUID of the user.
+ * @param isOnline Whether the user is currently online.
+ * @param userAddress The user's network address (nullable).
+ */
@Query("UPDATE conversations SET is_online = :isOnline, user_address = :userAddress WHERE user_uuid = :userUuid")
suspend fun updateUserConnectionStatus(userUuid: String, isOnline: Boolean, userAddress: String?)
+ /**
+ * Updates the last message and its timestamp for a specific conversation.
+ *
+ * @param conversationId The unique ID of the conversation.
+ * @param lastMessage The latest message text.
+ * @param timestamp The time when the last message was sent.
+ */
@Query("UPDATE conversations SET last_message = :lastMessage, last_message_time = :timestamp WHERE id = :conversationId")
suspend fun updateLastMessage(conversationId: String, lastMessage: String, timestamp: Long)
+ /**
+ * Increments the unread message count for a specific conversation by 1.
+ *
+ * @param conversationId The unique ID of the conversation.
+ */
@Query("UPDATE conversations SET unread_count = unread_count + 1 WHERE id = :conversationId")
suspend fun incrementUnreadCount(conversationId: String)
+ /**
+ * Clears the unread message count for a specific conversation, setting it to 0.
+ *
+ * @param conversationId The unique ID of the conversation.
+ */
@Query("UPDATE conversations SET unread_count = 0 WHERE id = :conversationId")
suspend fun clearUnreadCount(conversationId: String)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/data/dao/MessageDao.kt b/app/src/main/java/com/greybox/projectmesh/messaging/data/dao/MessageDao.kt
index 7afe14e0a..6dbe684e4 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/data/dao/MessageDao.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/data/dao/MessageDao.kt
@@ -7,34 +7,84 @@ import androidx.room.Query
import com.greybox.projectmesh.messaging.data.entities.Message
import kotlinx.coroutines.flow.Flow
+/**
+ * Data Access Object for [Message] entities.
+ *
+ * Provides methods to query, insert, and delete messages in the Room database.
+ */
@Dao
interface MessageDao {
+
+ /**
+ * Retrieves all messages as a synchronous list.
+ *
+ * @return A [List] of all [Message] objects in the database.
+ */
@Query("SELECT * FROM message")
fun getAll(): List
+ /**
+ * Returns a flow of all messages.
+ *
+ * @return [Flow] emitting a list of [Message] objects whenever the data changes.
+ */
@Query("SELECT * FROM message")
fun getAllFlow(): Flow>
+ /**
+ * Returns a flow of messages for a specific chat, ordered by date received ascending.
+ *
+ * @param chat The chat identifier.
+ * @return [Flow] emitting a list of [Message] objects for the given chat.
+ */
@Query("SELECT * FROM message WHERE chat = :chat ORDER BY dateReceived ASC")
fun getChatMessagesFlow(chat: String): Flow>
+ /**
+ * Deletes all messages from the database.
+ */
@Query("DELETE FROM message")
fun clearTable()
+ /**
+ * Returns a flow of messages for multiple chat names, ordered by date received ascending.
+ *
+ * @param chatNames A list of chat identifiers.
+ * @return [Flow] emitting a list of [Message] objects for the given chats.
+ */
@Query("SELECT * FROM message WHERE chat IN (:chatNames) ORDER BY dateReceived ASC")
fun getChatMessagesFlowMultipleNames(chatNames: List): Flow>
- //Synchronously Query to get messages immediately
+ /**
+ * Synchronously retrieves messages for a specific chat, ordered by date received ascending.
+ *
+ * @param chat The chat identifier.
+ * @return A [List] of [Message] objects for the given chat.
+ */
@Query("SELECT * FROM message WHERE chat = :chat ORDER BY dateReceived ASC")
fun getChatMessagesSync(chat: String): List
+ /**
+ * Inserts a new message into the database.
+ *
+ * @param m The [Message] to add.
+ */
@Insert
suspend fun addMessage(m: Message)
+ /**
+ * Deletes a single message from the database.
+ *
+ * @param m The [Message] to delete.
+ */
@Delete
fun delete(m: Message)
+ /**
+ * Deletes multiple messages from the database.
+ *
+ * @param messages The list of [Message] objects to delete.
+ */
@Delete
suspend fun deleteAll(messages: List)
-
}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/Conversation.kt b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/Conversation.kt
index aca71676b..4d649ca63 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/Conversation.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/Conversation.kt
@@ -5,8 +5,20 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.UUID
-//Conversation Entity, representing a chat thread with another user
-
+/**
+ * Represents a conversation (chat thread) with another user.
+ *
+ * Each conversation tracks the other user's info, the last message, unread count, and online status.
+ *
+ * @property id The unique ID of the conversation, typically a composite of the two users' IDs.
+ * @property userUuid The UUID of the other user in the conversation.
+ * @property userName The display name of the other user.
+ * @property userAddress The IP address of the other user (nullable).
+ * @property lastMessage The text of the last message in the conversation (nullable).
+ * @property lastMessageTime Timestamp of when the last message was sent.
+ * @property unreadCount The number of unread messages in this conversation (default 0).
+ * @property isOnline Indicates whether the other user is currently online (default false).
+ */
@Entity(tableName = "conversations")
data class Conversation(
@PrimaryKey val id: String, //Composite id of the two users
@@ -17,5 +29,4 @@ data class Conversation(
@ColumnInfo(name = "last_message_time") val lastMessageTime: Long, //Timestamp of last message
@ColumnInfo(name = "unread_count") val unreadCount: Int = 0, //count of unread messages
@ColumnInfo(name = "is_online") val isOnline: Boolean = false //whether the user is online
-
-)
\ No newline at end of file
+)
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/FileEncoder.kt b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/FileEncoder.kt
index 25bcfbf44..716d1d8d2 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/FileEncoder.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/FileEncoder.kt
@@ -53,68 +53,102 @@ import java.net.URL
import java.net.URLConnection
import java.net.URLDecoder
-//Use this to encode files not just images
-//Needs to be tested sometime
-//Can I modify this so that Http transfer does the majority of the encoding?
-class FileEncoder {//Made by Craig. Encodes via base64.
+/**
+ * Utility class for encoding and decoding files using Base64.
+ *
+ * This class provides functions to encode files to Base64 strings, decode
+ * Base64 strings back to files, and send files over HTTP as encoded strings.
+ */
+class FileEncoder { //Made by Craig. Encodes via base64.
+ /**
+ * Encodes the file located at the given URI into a Base64 string.
+ *
+ * @param ctxt The application context used to access the content resolver.
+ * @param inputuri The URI of the file to encode.
+ * @return The Base64-encoded string of the file contents, or a message
+ * indicating encoding failure.
+ */
@OptIn(ExperimentalEncodingApi::class)
- fun encodebase64(ctxt: Context, inputuri: Uri): String?{
- try {
+ fun encodebase64(ctxt: Context, inputuri: Uri): String? {
+ return try {
val encodedstrm: InputStream? = ctxt.contentResolver.openInputStream(inputuri)
val bytes = encodedstrm?.readBytes()
encodedstrm?.close()
- return if (bytes != null) {
- Base64.encode(bytes)
- } else {
- "Cannot encode file"
- }
- } catch(e: Exception){
+ encodeBytesBase64(bytes)
+ } catch (e: Exception) {
e.printStackTrace()
- return "Cannot encode file"}
-
+ "Cannot encode file"
+ }
}
- @OptIn(ExperimentalEncodingApi::class)//Made by Craig
- fun decodeBase64(inputbase64:String, output: File): File{//Decodes to a file. Uses base64
+ /**
+ * Decodes a Base64-encoded string and writes it to the specified file.
+ *
+ * @param inputbase64 The Base64 string to decode.
+ * @param output The file to write the decoded bytes to.
+ * @return The file containing the decoded data.
+ */
+ @OptIn(ExperimentalEncodingApi::class) //Made by Craig
+ fun decodeBase64(inputbase64:String, output: File): File { //Decodes to a file. Uses base64
val decodedfilebytes = Base64.decode(inputbase64)
val decodedstrm = FileOutputStream(output)
decodedstrm.write(decodedfilebytes)
decodedstrm.close()
return output
}
- fun sendImage(imageURI: Uri?, tgtaddress: InetAddress, tgtport:Int, appctxt: Context): Boolean{//Testing sending images
- try{//we can utilize this if we opt not to use JSON
+
+ @OptIn(ExperimentalEncodingApi::class)
+ internal fun encodeBytesBase64(bytes: ByteArray?): String? {
+ return if (bytes != null) {
+ Base64.encode(bytes)
+ } else {
+ "Cannot encode file"
+ }
+ }
+
+
+
+ /**
+ * Sends an image file to a target host and port using HTTP POST with Base64 encoding.
+ *
+ * @param imageURI The URI of the image to send. If null, the function returns false.
+ * @param tgtaddress The target host's InetAddress.
+ * @param tgtport The target port to send the image to.
+ * @param appctxt The application context used to access content resolver streams.
+ * @return True if the image was successfully sent, false otherwise.
+ */
+ fun sendImage(imageURI: Uri?, tgtaddress: InetAddress, tgtport:Int, appctxt: Context): Boolean { //Testing sending images
+ try { //we can utilize this if we opt not to use JSON
if(imageURI != null){
- val fp = encodebase64(appctxt, imageURI)//encodes file to base64
- if(!fp.equals("Cannot encode file")) {
- val efp = URLEncoder.encode(fp, "UTF-8")//ensures that the file URI is utf-8 encoded
- val connection =
- URL("http://${tgtaddress.hostAddress}:${tgtport}/upload?file=$efp").openConnection() as HttpURLConnection
- val request = "POST"//Specifies the request as a POST
- connection.doOutput = true
- connection.requestMethod = request
- connection.setChunkedStreamingMode(0)
- val instream = appctxt.contentResolver.openInputStream(imageURI)
- val outstream = connection.outputStream
- val readingbuffer = ByteArray(1024)
- var finishedreading: Int
- while (instream?.read(readingbuffer).also { finishedreading = it!! } != -1) {
- outstream.write(readingbuffer, 0, finishedreading)
+ val fp = encodebase64(appctxt, imageURI) //encodes file to base64
+ if(!fp.equals("Cannot encode file")) {
+ val efp = URLEncoder.encode(fp, "UTF-8") //ensures that the file URI is utf-8 encoded
+ val connection =
+ URL("http://${tgtaddress.hostAddress}:${tgtport}/upload?file=$efp").openConnection() as HttpURLConnection
+ val request = "POST" //Specifies the request as a POST
+ connection.doOutput = true
+ connection.requestMethod = request
+ connection.setChunkedStreamingMode(0)
+ val instream = appctxt.contentResolver.openInputStream(imageURI)
+ val outstream = connection.outputStream
+ val readingbuffer = ByteArray(1024)
+ var finishedreading: Int
+ while (instream?.read(readingbuffer).also { finishedreading = it!! } != -1) {
+ outstream.write(readingbuffer, 0, finishedreading)
+ }
+ outstream.close()
+ instream?.close()
+ } else {
+ return false
}
- outstream.close()
- instream?.close()
-
} else {
return false
- }} else {
- return false
}
- }
- catch(e: Exception){
+ } catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/JSONSchema.kt b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/JSONSchema.kt
index 69ce291f0..a14022939 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/JSONSchema.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/JSONSchema.kt
@@ -4,6 +4,11 @@ import android.util.Log
import org.json.JSONObject
import org.json.JSONException
+/**
+ * Utility class to validate JSON strings against a predefined JSON schema.
+ *
+ * The schema enforces required fields and data types for messages.
+ */
class JSONSchema {
private val schemaString = """
@@ -20,7 +25,13 @@ class JSONSchema {
}
}
"""
- //Takes JSON string and validates it against JSON Schema
+
+ /**
+ * Validates a JSON string against the internal schema.
+ *
+ * @param json The JSON string representing a message.
+ * @return True if the JSON is valid according to the schema, false otherwise.
+ */
fun schemaValidation(json: String): Boolean {
//Log.d("JSONSchema", "Validating JSON: $json")
//Log.d("JSONSchema", "Against schema: $schemaString")
@@ -30,13 +41,19 @@ class JSONSchema {
validate(jsonObject, schemaJson)
return true
- }catch (e: JSONException) {
+ } catch (e: JSONException) {
Log.e("JSONSchema", "JSON schema validation failed: ${e.message}")
return false
}
}
- //Validates JSON object against schema
+ /**
+ * Checks that the given JSON object contains all required fields as per the schema.
+ *
+ * @param json The JSON object to validate.
+ * @param schema The JSON schema object defining required fields.
+ * @throws JSONException If any required field is missing.
+ */
private fun validate(json: JSONObject, schema: JSONObject) {
val requiredFields = schema.getJSONArray("required")
for (i in 0 until requiredFields.length()) {
@@ -46,4 +63,4 @@ class JSONSchema {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/Message.kt b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/Message.kt
index f48afdd8f..f01f5f705 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/Message.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/Message.kt
@@ -13,34 +13,80 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.net.URI
-class URIConverter{
+
+/**
+ * Room type converter to convert between [URI] and [String] for database storage.
+ */
+class URIConverter {
+ /**
+ * Converts a [URI] to a [String] for database storage.
+ *
+ * @param theuri The URI to convert.
+ * @return The string representation of the URI, or null if input is null.
+ */
@TypeConverter
- fun convfromURI(theuri: URI?): String?{
+ fun convfromURI(theuri: URI?): String? {
return theuri?.toString()
}
+
+ /**
+ * Converts a [String] back to a [URI].
+ *
+ * @param uristring The string to convert.
+ * @return The corresponding URI, or null if input is null.
+ */
@TypeConverter
- fun convtoURI(uristring: String?): URI?{
- return uristring?.let{URI.create(it)}
+ fun convtoURI(uristring: String?): URI? {
+ return uristring?.let { URI.create(it) }
}
}
-object URISerializable : KSerializer {//This makes the URI serializable, can be used in JSON
+
+/**
+ * Serializer to make [URI] serializable for Kotlinx serialization (e.g., JSON).
+ */
+object URISerializable : KSerializer {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("URI", PrimitiveKind.STRING)
+
+ /**
+ * Serializes a [URI] into a string.
+ *
+ * @param enc The encoder.
+ * @param vals The URI to serialize.
+ */
override fun serialize(enc: Encoder, vals: URI) {
enc.encodeString(vals.toString())
}
+
+ /**
+ * Deserializes a string into a [URI].
+ *
+ * @param dec The decoder.
+ * @return The deserialized URI.
+ */
override fun deserialize(dec: Decoder): URI {
return URI.create(dec.decodeString())
}
}
+
+/**
+ * Room entity representing a message in a chat.
+ *
+ * @property id Unique message ID (auto-generated).
+ * @property dateReceived Timestamp when the message was received.
+ * @property content The text content of the message.
+ * @property sender The identifier of the sender.
+ * @property chat The chat/conversation ID this message belongs to.
+ * @property file Optional file attached to the message, stored as a [URI].
+ */
@Serializable
@Entity(tableName = "message")
@TypeConverters(URIConverter::class)
-data class Message(//
+data class Message(
@PrimaryKey(autoGenerate = true) val id: Int,
@ColumnInfo(name = "dateReceived") val dateReceived: Long,
@ColumnInfo(name = "content") val content: String,
@ColumnInfo(name = "sender") val sender: String,
@ColumnInfo(name = "chat") val chat: String,
- @ColumnInfo(name= "file") @Serializable(with=URISerializable::class) val file: URI? = null
- //@ColumnInfo(name = "file") @Serializable(with=URISerializable::class) val file: List
-)
\ No newline at end of file
+ @ColumnInfo(name = "file") @Serializable(with = URISerializable::class) val file: URI? = null
+ // @ColumnInfo(name = "file") @Serializable(with=URISerializable::class) val file: List
+)
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageNetworkHandler.kt b/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageNetworkHandler.kt
index 1a15b2f4f..01239b0c8 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageNetworkHandler.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageNetworkHandler.kt
@@ -36,6 +36,13 @@ import android.os.Parcel
import android.os.Parcelable
import java.net.URI
+/**
+ * Handles sending and receiving chat messages over the network.
+ *
+ * @property httpClient The OkHttpClient used to make HTTP requests.
+ * @property localVirtualAddr The local device's virtual network IP address.
+ * @property di The Kodein DI container instance for retrieving dependencies.
+ */
class MessageNetworkHandler(
private val httpClient: OkHttpClient,
private val localVirtualAddr: InetAddress,
@@ -45,7 +52,14 @@ class MessageNetworkHandler(
private val conversationRepository: ConversationRepository by di.instance()
private val settingsPrefs: SharedPreferences by di.instance(tag = "settings")
- //function sendChatMessage(address: InetAddress, time: Long, message: String) {
+ /**
+ * Sends a chat message to a remote device over HTTP.
+ *
+ * @param address The target device's IP address.
+ * @param time The timestamp of the message in milliseconds.
+ * @param message The message text to send.
+ * @param file Optional URI of a file to send along with the message.
+ */
fun sendChatMessage(address: InetAddress, time: Long, message: String, file: URI?/* test this*/) {
scope.launch {
try {
@@ -103,8 +117,17 @@ class MessageNetworkHandler(
}
}
}
+
companion object {
- //process incoming messages and route them to the correct conversation
+ /**
+ * Processes an incoming message and routes it to the correct conversation.
+ *
+ * @param chatMessage The message content received.
+ * @param time The timestamp when the message was received.
+ * @param senderIp The IP address of the sender.
+ * @param incomingfile Optional file URI attached to the message.
+ * @return The created [Message] object representing the incoming message.
+ */
fun handleIncomingMessage(
chatMessage: String?,
time: Long,
@@ -176,8 +199,13 @@ class MessageNetworkHandler(
return message
}
-
- // New helper function to show notifications that route to chat screen
+ /**
+ * Shows a notification for an incoming message and routes to the chat screen.
+ *
+ * @param conversation The conversation to which the message belongs.
+ * @param message The message to display in the notification.
+ * @param senderIp The IP address of the sender.
+ */
private fun showMessageNotification(
conversation: Conversation,
message: Message,
@@ -235,4 +263,4 @@ class MessageNetworkHandler(
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageService.kt b/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageService.kt
index 87317a376..ade62a252 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageService.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/network/MessageService.kt
@@ -15,6 +15,12 @@ import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.instance
+/**
+ * Service layer for handling message operations including sending messages
+ * and updating conversations.
+ *
+ * @property di Kodein DI container for retrieving required dependencies.
+ */
class MessageService(
override val di: DI
) : DIAware {
@@ -24,6 +30,13 @@ class MessageService(
private val userRepository: UserRepository by di.instance()
private val settingsPrefs: SharedPreferences by di.instance(tag = "settings")
+ /**
+ * Sends a message to a given IP address.
+ * First saves the message locally, then sends it over the network.
+ *
+ * @param address The target device's IP address.
+ * @param message The [Message] object to be sent.
+ */
suspend fun sendMessage(address: InetAddress, message: Message) {
//First save locally
messageRepository.addMessage(message)
@@ -37,6 +50,12 @@ class MessageService(
)
}
+ /**
+ * Updates the conversation associated with a given user IP with a new message.
+ *
+ * @param address The IP address of the remote user.
+ * @param message The [Message] object to update in the conversation.
+ */
private suspend fun updateConversationWithMessage(address: InetAddress, message: Message){
try {
//find user by ip address
@@ -64,4 +83,4 @@ class MessageService(
Log.e("MessageService", "Error updating conversation with message", e)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/repository/ConversationRepository.kt b/app/src/main/java/com/greybox/projectmesh/messaging/repository/ConversationRepository.kt
index 81e8e79c5..450d264a2 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/repository/ConversationRepository.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/repository/ConversationRepository.kt
@@ -10,17 +10,33 @@ import kotlinx.coroutines.flow.Flow
import org.kodein.di.DI
import org.kodein.di.DIAware
+/**
+ * Repository for managing conversations.
+ * Handles retrieval, creation, updating, and user status tracking for conversations.
+ *
+ * @property conversationDao DAO for database operations related to conversations.
+ * @property di Kodein DI container for dependency injection.
+ */
class ConversationRepository(
private val conversationDao: ConversationDao,
override val di: DI
) : DIAware {
- //get all conversations as a flow
+ /**
+ * Returns a [Flow] emitting the list of all conversations.
+ *
+ * @return A [Flow] of [List] of [Conversation].
+ */
fun getAllConversations(): Flow>{
return conversationDao.getAllConversationsFlow()
}
- //get specific convo by id
+ /**
+ * Retrieves a conversation by its unique ID.
+ *
+ * @param conversationId The unique ID of the conversation.
+ * @return The [Conversation] if found, null otherwise.
+ */
suspend fun getConversationById(conversationId: String): Conversation? {
Log.d("ConversationRepository", "Getting conversation by ID: $conversationId")
val result = conversationDao.getConversationById(conversationId)
@@ -28,6 +44,13 @@ class ConversationRepository(
return result
}
+ /**
+ * Retrieves an existing conversation or creates a new one if it does not exist.
+ *
+ * @param localUuid The UUID of the local user.
+ * @param remoteUser The remote user entity to associate with the conversation.
+ * @return The existing or newly created [Conversation].
+ */
suspend fun getOrCreateConversation(localUuid: String, remoteUser: UserEntity): Conversation {
//create a unique conversation ID using both UUIDs in order to ensure consistency
val conversationId = ConversationUtils.createConversationId(localUuid, remoteUser.uuid)
@@ -60,7 +83,13 @@ class ConversationRepository(
return conversation
}
- //update conversation with the latest message
+ /**
+ * Updates the conversation with the latest message.
+ * Increments unread count if the message is from another user.
+ *
+ * @param conversationId The unique ID of the conversation.
+ * @param message The [Message] to update in the conversation.
+ */
suspend fun updateWithMessage(conversationId: String, message: Message) {
conversationDao.updateLastMessage(
@@ -82,12 +111,22 @@ class ConversationRepository(
}
- //mark conversation as read
+ /**
+ * Marks a conversation as read by clearing its unread count.
+ *
+ * @param conversationId The unique ID of the conversation.
+ */
suspend fun markAsRead(conversationId: String) {
conversationDao.clearUnreadCount(conversationId)
}
- //update a user's online status
+ /**
+ * Updates a user's online status and associated address.
+ *
+ * @param userUuid The UUID of the user.
+ * @param isOnline True if the user is online, false otherwise.
+ * @param userAddress The current address of the user, if available.
+ */
suspend fun updateUserStatus(userUuid: String, isOnline: Boolean, userAddress: String?) {
try {
// Update in database
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/repository/MessageRepository.kt b/app/src/main/java/com/greybox/projectmesh/messaging/repository/MessageRepository.kt
index adf8b069f..4ca0b759b 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/repository/MessageRepository.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/repository/MessageRepository.kt
@@ -1,4 +1,3 @@
-// Path: app/src/main/java/com/greybox/projectmesh/messaging/repository/MessageRepository.kt
package com.greybox.projectmesh.messaging.repository
import com.greybox.projectmesh.messaging.data.dao.MessageDao
@@ -8,27 +7,51 @@ import org.kodein.di.DI
import org.kodein.di.DIAware
// Changed to use Kodein instead of javax.inject
+
+/**
+ * Repository for managing messages in the app.
+ * Handles retrieval, insertion, and clearing of messages for chats.
+ *
+ * @property messageDao DAO for database operations related to messages.
+ * @property di Kodein DI container for dependency injection.
+ */
class MessageRepository(
private val messageDao: MessageDao,
override val di: DI
) : DIAware {
- // Get all messages for a chat
+
+ /**
+ * Retrieves all messages for a specific chat as a [Flow].
+ *
+ * @param chatId The ID of the chat to retrieve messages for.
+ * @return A [Flow] emitting a [List] of [Message] objects for the chat.
+ */
fun getChatMessages(chatId: String): Flow> {
return messageDao.getChatMessagesFlow(chatId)
}
- // Add a new message
+ /**
+ * Adds a new message to the database.
+ *
+ * @param message The [Message] to add.
+ */
suspend fun addMessage(message: Message) {
messageDao.addMessage(message)
}
- // Get all messages
+ /**
+ * Retrieves all messages from the database as a [Flow].
+ *
+ * @return A [Flow] emitting a [List] of all [Message] objects.
+ */
fun getAllMessages(): Flow> {
return messageDao.getAllFlow()
}
- // Clear all messages
+ /**
+ * Clears all messages from the database.
+ */
suspend fun clearMessages() {
messageDao.clearTable()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/ui/models/ChatScreenModel.kt b/app/src/main/java/com/greybox/projectmesh/messaging/ui/models/ChatScreenModel.kt
index a0e5b60bf..67a7c358e 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/ui/models/ChatScreenModel.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/ui/models/ChatScreenModel.kt
@@ -3,9 +3,17 @@ package com.greybox.projectmesh.messaging.ui.models
import com.greybox.projectmesh.messaging.data.entities.Message
import java.net.InetAddress
+/**
+ * Data model representing the state of a chat screen in the UI.
+ *
+ * @property deviceName Optional name of the device or user.
+ * @property virtualAddress Virtual network address of the device; defaults to 192.168.0.1.
+ * @property allChatMessages List of all messages to display on the chat screen; defaults to empty list.
+ * @property offlineWarning Optional warning message to show if the device/user is offline.
+ */
data class ChatScreenModel(
val deviceName: String? = null,
val virtualAddress: InetAddress = InetAddress.getByName("192.168.0.1"),
val allChatMessages: List = emptyList(),
val offlineWarning: String? = null
-)
\ No newline at end of file
+)
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/ui/models/ConversationsHomeScreenModel.kt b/app/src/main/java/com/greybox/projectmesh/messaging/ui/models/ConversationsHomeScreenModel.kt
index 2c3fd30d5..227b2b69e 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/ui/models/ConversationsHomeScreenModel.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/ui/models/ConversationsHomeScreenModel.kt
@@ -2,8 +2,15 @@ package com.greybox.projectmesh.messaging.ui.models
import com.greybox.projectmesh.messaging.data.entities.Conversation
+/**
+ * Data model representing the state of the home screen showing all conversations.
+ *
+ * @property isLoading Indicates whether conversation data is currently being loaded.
+ * @property conversations List of conversations to display on the home screen; defaults to empty list.
+ * @property error Optional error message to display if loading or retrieving conversations fails.
+ */
data class ConversationsHomeScreenModel (
val isLoading: Boolean = false,
val conversations: List = emptyList(),
val error: String? = null
-)
\ No newline at end of file
+)
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatNodeListScreen.kt b/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatNodeListScreen.kt
index 90c1b3d03..78fdfea1b 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatNodeListScreen.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatNodeListScreen.kt
@@ -13,6 +13,13 @@ import org.kodein.di.compose.localDI
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
import com.greybox.projectmesh.viewModel.NetworkScreenModel
+/**
+ * Composable that displays a list of network nodes as clickable items.
+ *
+ * @param onNodeSelected Lambda invoked when a node is selected; passes the node's IP address as a [String].
+ * @param viewModel Optional [NetworkScreenViewModel] instance to provide network node data.
+ * Defaults to a ViewModel created with [ViewModelFactory] using the local DI context.
+ */
@Composable
fun ChatNodeListScreen(
onNodeSelected: (String) -> Unit,
@@ -46,4 +53,4 @@ fun ChatNodeListScreen(
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatScreen.kt b/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatScreen.kt
index 24f690dd4..7b0ad6d64 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatScreen.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ChatScreen.kt
@@ -74,6 +74,15 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
+/**
+ * Composable function representing the main chat screen.
+ *
+ * @param virtualAddress The IP address of the chat participant.
+ * @param userName Optional username for the chat participant.
+ * @param isOffline Boolean flag indicating if the user is offline.
+ * @param onClickButton Callback for button click events.
+ * @param viewModel The [ChatScreenViewModel] providing UI state and actions.
+ */
@Composable
fun ChatScreen(
virtualAddress: InetAddress,
@@ -89,7 +98,6 @@ fun ChatScreen(
},
defaultArgs = Bundle().apply {
putSerializable("virtualAddress", virtualAddress)
-
}
)
)
@@ -276,6 +284,13 @@ fun ChatScreen(
}
}
+/**
+ * Composable function showing the user's status bar at the top of the chat.
+ *
+ * @param userName Name of the chat participant.
+ * @param isOnline Boolean flag indicating online/offline status.
+ * @param userAddress IP address of the chat participant.
+ */
@Composable
fun UserStatusBar(
userName: String,
@@ -366,6 +381,12 @@ fun UserStatusBar(
}
}
+/**
+ * Composable function displaying all messages in the chat.
+ *
+ * @param uiState [ChatScreenModel] representing the current state of the chat.
+ * @param onClickButton Callback for any button interactions within the messages list.
+ */
@Composable
fun DisplayAllMessages(uiState: ChatScreenModel, onClickButton: () -> Unit) {
val context = LocalContext.current
@@ -441,6 +462,15 @@ fun DisplayAllMessages(uiState: ChatScreenModel, onClickButton: () -> Unit) {
}
}
+/**
+ * Composable function displaying an individual message bubble.
+ *
+ * @param chatMessage The [Message] object containing message data.
+ * @param sentBySelf Boolean indicating whether the message was sent by the current user.
+ * @param messageContent Composable lambda for rendering the message content.
+ * @param sender Name of the sender of the message.
+ * @param modifier Modifier to apply to the message bubble.
+ */
@Composable
fun MessageBubble(
chatMessage: Message,
@@ -510,4 +540,3 @@ fun MessageBubble(
}
}
}
-
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ConversationsHomeScreen.kt b/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ConversationsHomeScreen.kt
index 371fe36f1..4af4200a9 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ConversationsHomeScreen.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/ui/screens/ConversationsHomeScreen.kt
@@ -32,6 +32,12 @@ import com.greybox.projectmesh.messaging.ui.viewmodels.ConversationsHomeScreenVi
import com.greybox.projectmesh.messaging.utils.MessageUtils
import org.kodein.di.compose.localDI
+/**
+ * Main Composable for the Conversations Home screen.
+ *
+ * @param onConversationSelected Callback when a conversation is selected.
+ * @param viewModel [ConversationsHomeScreenViewModel] providing the UI state.
+ */
@Composable
fun ConversationsHomeScreen(
onConversationSelected: (String) -> Unit,
@@ -88,6 +94,12 @@ fun ConversationsHomeScreen(
}
}
+/**
+ * Displays a scrollable list of conversations.
+ *
+ * @param conversations List of [Conversation] objects to display.
+ * @param onConversationClick Callback when a conversation item is clicked.
+ */
@Composable
fun ConversationsList(
conversations: List,
@@ -110,6 +122,12 @@ fun ConversationsList(
}
}
+/**
+ * Displays an individual conversation item with avatar, status, last message, and unread count.
+ *
+ * @param conversation The [Conversation] to display.
+ * @param onClick Callback for when the conversation item is clicked.
+ */
@Composable
fun ConversationItem(
conversation: Conversation,
@@ -276,6 +294,9 @@ fun ConversationItem(
}
}
+/**
+ * Displays a placeholder view when there are no conversations.
+ */
@Composable
fun EmptyConversationsView() {
Column(
@@ -310,6 +331,12 @@ fun EmptyConversationsView() {
}
}
+/**
+ * Displays an error view with retry button when conversation loading fails.
+ *
+ * @param errorMessage The error message to display.
+ * @param onRetry Callback triggered when retry button is pressed.
+ */
@Composable
fun ErrorView(
errorMessage: String,
@@ -359,4 +386,4 @@ fun ErrorView(
Text("Retry")
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ChatScreenViewModel.kt b/app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ChatScreenViewModel.kt
index 68448f703..0f52fe6f7 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ChatScreenViewModel.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ChatScreenViewModel.kt
@@ -38,10 +38,19 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withTimeoutOrNull
import java.net.URI
+/**
+ * ViewModel for the Chat Screen.
+ *
+ * Responsible for managing chat messages, device status, and conversation information.
+ *
+ * @param di Dependency Injection container to provide required services and repositories.
+ * @param savedStateHandle Handles saved state, including virtualAddress and conversationId.
+ */
class ChatScreenViewModel(
di: DI,
savedStateHandle: SavedStateHandle
) : ViewModel() {
+
private val virtualAddress: InetAddress = savedStateHandle.get("virtualAddress")!!
// _uiState will be updated whenever there is a change in the UI state
@@ -78,18 +87,15 @@ class ChatScreenViewModel(
//Log.d("ChatDebug", "GOT CONVERSATION ID FROM SAVED STATE: $savedConversationId")
private val conversationId = passedConversationId ?:
- ConversationUtils.createConversationId(localUuid, userUuid)
+ ConversationUtils.createConversationId(localUuid, userUuid)
private val chatName = savedConversationId ?: conversationId
//Log.d("ChatDebug", "USING CHAT NAME: $chatName (saved: $savedConversationId, generated: $conversationId)")
-
-
private val addressDotNotation = virtualAddress.requireAddressAsInt().addressToDotNotation()
private val conversationRepository: ConversationRepository by di.instance()
-
private val _uiState = MutableStateFlow(
ChatScreenModel(
deviceName = deviceName,
@@ -254,7 +260,13 @@ class ChatScreenViewModel(
}
}
-
+ /**
+ * Sends a chat message to a virtual device.
+ *
+ * @param virtualAddress IP address of the target device.
+ * @param message Message content as String.
+ * @param file Optional file attachment as [URI].
+ */
fun sendChatMessage(
virtualAddress: InetAddress,
message: String,
@@ -341,7 +353,13 @@ class ChatScreenViewModel(
}
}
- //handles outgoing file transfer to fix unresolved reference error crash
+ /**
+ * Adds an outgoing file transfer for a given device.
+ *
+ * @param fileUri [Uri] of the file to send.
+ * @param toAddress IP address of the target device.
+ * @return [OutgoingTransferInfo] containing details of the transfer.
+ */
fun addOutgoingTransfer(fileUri: Uri, toAddress: InetAddress): OutgoingTransferInfo {
return appServer.addOutgoingTransfer(fileUri, toAddress)
}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ConversationsHomeScreenViewModel.kt b/app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ConversationsHomeScreenViewModel.kt
index 9fd2933e1..7fcf248c9 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ConversationsHomeScreenViewModel.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/ui/viewmodels/ConversationsHomeScreenViewModel.kt
@@ -17,6 +17,14 @@ import kotlinx.coroutines.launch
import org.kodein.di.DI
import org.kodein.di.instance
+/**
+ * ViewModel for the Conversations Home Screen.
+ *
+ * Manages the list of conversations, updates device online/offline statuses,
+ * and provides functions for refreshing and marking conversations as read.
+ *
+ * @param di Dependency Injection container to provide required repositories and settings.
+ */
class ConversationsHomeScreenViewModel(
di: DI
) : ViewModel() {
@@ -88,7 +96,6 @@ class ConversationsHomeScreenViewModel(
}
}
-
private fun loadConversations() {
viewModelScope.launch {
try {
@@ -139,12 +146,20 @@ class ConversationsHomeScreenViewModel(
}
}
- //function to refresh conversations manually
+ /**
+ * Refreshes the conversations list manually.
+ *
+ * Reloads the conversations from the repository.
+ */
fun refreshConversations(){
loadConversations()
}
- //Function to mark a conversation as read
+ /**
+ * Marks a conversation as read.
+ *
+ * @param conversationId The ID of the conversation to mark as read.
+ */
fun markConversationAsRead(conversationId: String) {
viewModelScope.launch {
try {
@@ -154,4 +169,4 @@ class ConversationsHomeScreenViewModel(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/utils/Logger.kt b/app/src/main/java/com/greybox/projectmesh/messaging/utils/Logger.kt
index 9000bc113..0323b054b 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/utils/Logger.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/utils/Logger.kt
@@ -6,45 +6,84 @@ import android.util.Log
* Centralized logging utility for the app.
* Provides consistent logging with standardized tags and can be disabled in production.
*/
-
object Logger {
+ internal const val TAG_PREFIX = "MeshChat_"
private const val LOGGING_ENABLED = true
- private const val TAG_PREFIX = "MeshChat_"
+ internal fun buildTag(tag: String): String {
+ return "$TAG_PREFIX$tag"
+ }
+
+ internal fun buildCriticalTag(tag: String): String {
+ return "${TAG_PREFIX}${tag}_CRITICAL"
+ }
+
+ /**
+ * Logs a debug-level message.
+ *
+ * @param tag The log tag used to identify the source.
+ * @param message The message to log.
+ */
fun d(tag: String, message: String) {
if (LOGGING_ENABLED) {
- Log.d("$TAG_PREFIX$tag", message)
+ Log.d(buildTag(tag), message)
}
}
+ /**
+ * Logs an info-level message.
+ *
+ * @param tag The log tag used to identify the source.
+ * @param message The message to log.
+ */
fun i(tag: String, message: String) {
if (LOGGING_ENABLED) {
- Log.i("$TAG_PREFIX$tag", message)
+ Log.i(buildTag(tag), message)
}
}
+ /**
+ * Logs a warning-level message.
+ *
+ * @param tag The log tag used to identify the source.
+ * @param message The message to log.
+ */
fun w(tag: String, message: String) {
if (LOGGING_ENABLED) {
- Log.w("$TAG_PREFIX$tag", message)
+ Log.w(buildTag(tag), message)
}
}
+ /**
+ * Logs an error-level message.
+ *
+ * @param tag The log tag used to identify the source.
+ * @param message The message to log.
+ * @param throwable Optional exception to include in the log output.
+ */
fun e(tag: String, message: String, throwable: Throwable? = null) {
if (LOGGING_ENABLED) {
if (throwable != null) {
- Log.e("$TAG_PREFIX$tag", message, throwable)
+ Log.e(buildTag(tag), message, throwable)
} else {
- Log.e("$TAG_PREFIX$tag", message)
+ Log.e(buildTag(tag), message)
}
}
}
- // Log important events that should be visible even in production
+ /**
+ * Logs high-importance errors that should always appear even in production.
+ *
+ * @param tag The log tag used to identify the source.
+ * @param message The message to log.
+ * @param throwable Optional exception to include in the log output.
+ */
fun critical(tag: String, message: String, throwable: Throwable? = null) {
+ val criticalTag = buildCriticalTag(tag)
if (throwable != null) {
- Log.e("$TAG_PREFIX${tag}_CRITICAL", message, throwable)
+ Log.e(criticalTag, message, throwable)
} else {
- Log.e("$TAG_PREFIX${tag}_CRITICAL", message)
+ Log.e(criticalTag, message)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtils.kt b/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtils.kt
index a2bb342f4..d93a61c96 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtils.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtils.kt
@@ -12,15 +12,35 @@ import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.instance
+/**
+ * Utility responsible for migrating legacy messages to the newer
+ * conversation-ID–based chat naming format.
+ *
+ * It inspects all existing messages, infers correct conversation IDs,
+ * and rewrites their `chat` field when necessary.
+ *
+ * This allows older installations to transition cleanly to the
+ * standardized conversation model.
+ */
class MessageMigrationUtils(
override val di: DI
): DIAware {
+
private val db: MeshDatabase by di.instance()
/**
- * Migrates existing messages to use in converstion IDs as chatNames
+ * Migrates all historical messages so that each message's `chat`
+ * value follows the modern conversation ID format.
+ *
+ * Steps performed:
+ * - Loads all messages
+ * - Groups them by legacy chat name
+ * - Determines correct UUID association for each chat group
+ * - Generates a conversation ID using local + remote UUIDs
+ * - Rewrites messages with updated chat names
+ *
+ * Errors are logged but do not stop the migration process.
*/
-
suspend fun migrateMessagesToChatIds() {
withContext(Dispatchers.IO){
try {
@@ -93,7 +113,17 @@ class MessageMigrationUtils(
}
}
- private fun createConversationId(uuid1: String, uuid2: String): String {
+ /**
+ * Generates a consistent conversation ID using two UUIDs.
+ *
+ * Special cases:
+ * - Test device UUIDs map to fixed, readable conversation IDs.
+ *
+ * @param uuid1 Local UUID.
+ * @param uuid2 Remote UUID.
+ * @return A stable, sorted, hyphen-joined conversation ID.
+ */
+ internal fun createConversationId(uuid1: String, uuid2: String): String {
// Special cases for test devices
if (uuid2 == "test-device-uuid") {
return "local-user-test-device-uuid"
@@ -103,4 +133,4 @@ class MessageMigrationUtils(
}
return listOf(uuid1, uuid2).sorted().joinToString("-")
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageUtils.kt b/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageUtils.kt
index af2d20275..e9db5b825 100644
--- a/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageUtils.kt
+++ b/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageUtils.kt
@@ -1,13 +1,34 @@
package com.greybox.projectmesh.messaging.utils
+/**
+ * Utility functions for formatting message metadata and generating stable
+ * chat identifiers used throughout the messaging system.
+ */
object MessageUtils {
+
+ /**
+ * Formats a Unix timestamp into a human-readable time string.
+ *
+ * @param timestamp The timestamp in milliseconds.
+ * @return A formatted time string in `"HH:mm"` format.
+ */
fun formatTimestamp(timestamp: Long): String {
//Adding timestamp formatting logic
return java.text.SimpleDateFormat("HH:mm").format(timestamp)
}
+ /**
+ * Generates a stable, deterministic chat ID from two user identifiers.
+ *
+ * The two identifiers are sorted alphabetically so both users
+ * will always compute the same ID for the same pair.
+ *
+ * @param sender The identifier of the sender.
+ * @param receiver The identifier of the receiver.
+ * @return A hyphen-joined chat ID such as `"userA-userB"`.
+ */
fun generateChatId(sender: String, receiver: String): String {
//Create a consistent chat ID for two users
return listOf(sender, receiver).sorted().joinToString("-")
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavHost.kt b/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavHost.kt
index 959ed9433..a5ba77f43 100644
--- a/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavHost.kt
+++ b/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavHost.kt
@@ -13,6 +13,13 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.compose.rememberNavController
import com.greybox.projectmesh.R
+/**
+ * Represents a single item inside the bottom navigation bar.
+ *
+ * @param route The navigation route associated with this item.
+ * @param label The text label shown beneath the icon.
+ * @param icon The vector icon displayed for this item.
+ */
data class NavigationItem(
val route: String,
val label: String,
@@ -20,6 +27,9 @@ data class NavigationItem(
)
//Preview is to show the bottom navigation bar in the preview and notice what it looks like
+/**
+ * Preview for displaying the bottom navigation bar inside the design tools.
+ */
@Preview(showBackground = true)
@Composable
fun BottomNavigationBarPreview() {
@@ -27,6 +37,14 @@ fun BottomNavigationBarPreview() {
BottomNavigationBar(navController = navController)
}
+/**
+ * Displays the application's bottom navigation bar.
+ *
+ * Automatically highlights the currently selected destination and handles
+ * navigation state restoration and avoiding duplicate destinations.
+ *
+ * @param navController The controller used to perform navigation actions.
+ */
@Composable
fun BottomNavigationBar(navController: NavHostController) {
val items = listOf(
@@ -66,8 +84,8 @@ fun BottomNavigationBar(navController: NavHostController) {
launchSingleTop = true
}
},
-
+
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavItem.kt b/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavItem.kt
index 83e8b9351..437cd8fff 100644
--- a/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavItem.kt
+++ b/app/src/main/java/com/greybox/projectmesh/navigation/BottomNavItem.kt
@@ -5,13 +5,31 @@ import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.*
import androidx.compose.ui.graphics.vector.ImageVector
-
-sealed class BottomNavItem(val route: String, val title: String, val icon: ImageVector){
+/**
+ * Represents a single item in the bottom navigation bar.
+ *
+ * Each item has a route (for navigation), a title (displayed as text),
+ * and an icon (displayed visually in the bar).
+ */
+sealed class BottomNavItem(val route: String, val title: String, val icon: ImageVector) {
+ /** Home tab item */
data object Home : BottomNavItem("home", "Home", Icons.Default.Home)
+
+ /** Network tab item */
data object Network : BottomNavItem("network", "Network", Icons.Default.Wifi)
+
+ /** Send tab item */
data object Send : BottomNavItem("send", "Send", Icons.AutoMirrored.Filled.Send)
+
+ /** Receive tab item */
data object Receive : BottomNavItem("receive", "Receive", Icons.Default.Download)
+
+ /** Log tab item */
data object Log: BottomNavItem("log", "Log", Icons.Default.History)
+
+ /** Settings tab item */
data object Settings : BottomNavItem("settings", "Settings", Icons.Default.Settings)
+
+ /** Chat tab item */
data object Chat : BottomNavItem("chat", "Chat", Icons.Default.ChatBubble)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/server/InputStreamCounter.kt b/app/src/main/java/com/greybox/projectmesh/server/InputStreamCounter.kt
index 256ba9422..df4821c7c 100644
--- a/app/src/main/java/com/greybox/projectmesh/server/InputStreamCounter.kt
+++ b/app/src/main/java/com/greybox/projectmesh/server/InputStreamCounter.kt
@@ -22,13 +22,6 @@ class InputStreamCounter(
}
}
- override fun read(b: ByteArray): Int {
- return super.read(b).also {
- if(it != -1)
- bytesRead += it
- }
- }
-
override fun read(b: ByteArray, off: Int, len: Int): Int {
return super.read(b, off, len).also {
if(it != -1)
diff --git a/app/src/main/java/com/greybox/projectmesh/testing/README.md b/app/src/main/java/com/greybox/projectmesh/testing/README.md
index d6a22a482..7b4f428cb 100644
--- a/app/src/main/java/com/greybox/projectmesh/testing/README.md
+++ b/app/src/main/java/com/greybox/projectmesh/testing/README.md
@@ -9,17 +9,17 @@ Project Mesh includes built-in test users to help with development and testing.
The application includes two test users:
1. **Online Test Device**
- - Name: "Test Echo Device (Online)"
- - IP Address: 192.168.0.99
- - Status: Always appears as online
- - Behavior: Automatically responds to messages with an echo reply
+ - Name: "Test Echo Device (Online)"
+ - IP Address: 192.168.0.99
+ - Status: Always appears as online
+ - Behavior: Automatically responds to messages with an echo reply
2. **Offline Test Device**
- - Name: "Test Echo Device (Offline)"
- - IP Address: 192.168.0.98
- - Status: Always appears as offline
- - Status: Always appears as offline
- - Behavior: Messages can be sent but will remain stored locally
+ - Name: "Test Echo Device (Offline)"
+ - IP Address: 192.168.0.98
+ - Status: Always appears as offline
+ - Status: Always appears as offline
+ - Behavior: Messages can be sent but will remain stored locally
**Only Online User Shows Up as Online**:
@@ -35,6 +35,7 @@ The application includes two test users:
## Usage Examples
### Testing Message Delivery
+
1. Navigate to the Chat screen
2. Select "Test Echo Device (Online)" from the conversation list
3. Send a message
@@ -43,6 +44,7 @@ The application includes two test users:
### Testing Offline Message Behavior
+
1. Navigate to the Chat screen
2. Select "Test Echo Device (Offline)" from the conversation list
3. Send a message
@@ -74,9 +76,12 @@ const val TEST_DEVICE_NAME_OFFLINE = "Test Echo Device (Offline)"
## Test User Integration
### How Test Users Connect with the Project Mesh Architecture
+
Test users are integrated into several key components of the Project Mesh architecture to simulate real devices without requiring actual physical connections. Here's how they interface with the core systems:
+
1. TestDeviceService Integration
-```kotlin
+
+```kotlin
// In TestDeviceService.kt
companion object {
const val TEST_DEVICE_IP = "192.168.0.99"
@@ -119,22 +124,24 @@ companion object {
Log.e("TestDeviceService", "Failed to initialize test device", e)
}
}
-
+
// Additional methods for test device functionality...
}
```
+
2. Global App Integration
-```kotlin
+
+```kotlin
// In GlobalApp.kt
override fun onCreate() {
super.onCreate()
-
+
// Other initialization code...
//Initialize test device:
TestDeviceService.initialize()
Log.d("MainActivity", "Test device initialized")
-
+
// Test conversation setup
insertTestConversations()
}
@@ -197,8 +204,10 @@ fun insertTestConversations() {
}
}
```
-3. AppServer Integration
-```kotlin
+
+3. AppServer Integration
+
+```kotlin
// In AppServer.kt
fun sendChatMessageWithStatus(address: InetAddress, time: Long, message: String, f: URI?): Boolean {
try {
@@ -218,7 +227,7 @@ fun sendChatMessageWithStatus(address: InetAddress, time: Long, message: String,
Log.d("AppServer", "Test device echoed message: $message")
return true
}
-
+
// Normal chat message handling for real devices...
}
catch (e: Exception) {
@@ -241,22 +250,24 @@ fun requestRemoteUserInfo(remoteAddr: InetAddress, port: Int = DEFAULT_PORT) {
DeviceStatusManager.updateDeviceStatus(ipAddress, false)
return
}
-
+
// Normal remote user info handling for real devices...
}
```
-4. DeviceStatusManager Integration
-```kotlin
+
+4. DeviceStatusManager Integration
+
+```kotlin
// In DeviceStatusManager.kt
object DeviceStatusManager {
// Other properties and methods...
-
+
//special test device addresses that should be handled differently
private val specialDevices = setOf(
"192.168.0.99", // Online test device
"192.168.0.98" // Offline test device
)
-
+
fun updateDeviceStatus(ipAddress: String, isOnline: Boolean, verified: Boolean = false) {
//if this is a special device, handle according to its predefined status
if (ipAddress == "192.168.0.99") { // Online test device
@@ -276,21 +287,23 @@ object DeviceStatusManager {
Log.d("DeviceStatusManager", "Updated test device status for $ipAddress: offline")
return
}
-
+
// Normal device status handling for real devices...
}
-
+
fun verifyDeviceStatus(ipAddress: String) {
// Skip verification for special test devices
if (ipAddress in specialDevices) {
return
}
-
+
// Normal device verification for real devices...
}
}
```
+
5. NetworkServiceViewModel Integration
+
```kotlin
// In NetworkScreenViewModel.kt
init {
@@ -313,21 +326,23 @@ init {
}
}
```
+
6. ConversationsHomeScreen Integration
Test devices appear in the conversations list as either online or offline contacts, with special handling to ensure they have the correct status regardless of actual network conditions.
7. ChatScreen Integration
-```kotlin
+
+```kotlin
// In ChatScreenViewModel.kt
fun sendChatMessage(virtualAddress: InetAddress, message: String, file: URI?) {
// Other processing...
-
+
viewModelScope.launch {
//save to local database
db.messageDao().addMessage(messageEntity)
//update convo with the new message
// ...
-
+
if (isOnline) {
try {
// Send message to real device
@@ -340,4 +355,4 @@ fun sendChatMessage(virtualAddress: InetAddress, message: String, file: URI?) {
}
```
-The test user system integrates smoothly with the existing architecture by implementing specialized handling at key decision points throughout the codebase. This allows the test devices to behave consistently and predictably while reusing much of the same code path as real devices.
\ No newline at end of file
+The test user system integrates smoothly with the existing architecture by implementing specialized handling at key decision points throughout the codebase. This allows the test devices to behave consistently and predictably while reusing much of the same code path as real devices.
diff --git a/app/src/main/java/com/greybox/projectmesh/testing/TestDeviceEntry.kt b/app/src/main/java/com/greybox/projectmesh/testing/TestDeviceEntry.kt
index d78b4a0c1..8a80ce495 100644
--- a/app/src/main/java/com/greybox/projectmesh/testing/TestDeviceEntry.kt
+++ b/app/src/main/java/com/greybox/projectmesh/testing/TestDeviceEntry.kt
@@ -9,14 +9,25 @@ import java.net.DatagramSocket
import java.net.InetAddress
import java.util.concurrent.Executors
+/**
+ * Utility class to create test device entries for the mesh network.
+ *
+ * This simulates a device with a virtual node, logger, and mock network socket,
+ * allowing for testing without real devices.
+ */
class TestDeviceEntry {
companion object {
- // Create a test logger
+ /** Test logger used for capturing logs during testing */
private val testLogger = TestMNetLogger()
+ /**
+ * Creates a simulated test device entry.
+ *
+ * @return a pair containing the device's integer address and its LastOriginatorMessage
+ */
fun createTestEntry(): Pair {
try {
- //convert string IP to bytes
+ // Convert the string IP to a byte array
val testAddressBytes = TestDeviceService.TEST_DEVICE_IP
.split(".")
.map { it.toInt().toByte() }
@@ -24,7 +35,7 @@ class TestDeviceEntry {
val testAddress = InetAddress.getByAddress(testAddressBytes)
- // Convert IP address to Int manually
+ // Convert IP address bytes to an Int manually
val testAddressInt = testAddressBytes.foldIndexed(0) { index, acc, byte ->
acc or ((byte.toInt() and 0xFF) shl (24 - (index * 8)))
}
@@ -32,8 +43,7 @@ class TestDeviceEntry {
Log.d("TestDeviceEntry", "Creating test entry with IP: ${TestDeviceService.TEST_DEVICE_IP}")
Log.d("TestDeviceEntry", "Test address as int: $testAddressInt")
-
- //create basic MmcpOriginatorMessage
+ // Create a basic MmcpOriginatorMessage
val mockOriginatorMessage = MmcpOriginatorMessage(
messageId = 1,
pingTimeSum = 50.toShort(),
@@ -41,10 +51,10 @@ class TestDeviceEntry {
sentTime = System.currentTimeMillis()
)
- //create a virtual router for testing
+ // Create a virtual router for testing
val testRouter = TestVirtualRouter()
- //create a mock VirtualNodeDatagramSocket with our test router
+ // Create a mock VirtualNodeDatagramSocket using our test router
val mockSocket = VirtualNodeDatagramSocket(
socket = DatagramSocket(),
ioExecutorService = Executors.newSingleThreadExecutor(),
@@ -53,7 +63,7 @@ class TestDeviceEntry {
logger = testLogger
)
- // Create LastOriginatorMessage with all required parameters
+ // Build the LastOriginatorMessage object
val lastOriginatorMessage = VirtualNode.LastOriginatorMessage(
originatorMessage = mockOriginatorMessage,
timeReceived = System.currentTimeMillis(),
@@ -71,4 +81,4 @@ class TestDeviceEntry {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/testing/TestDeviceService.kt b/app/src/main/java/com/greybox/projectmesh/testing/TestDeviceService.kt
index 34754bebb..86579c6e1 100644
--- a/app/src/main/java/com/greybox/projectmesh/testing/TestDeviceService.kt
+++ b/app/src/main/java/com/greybox/projectmesh/testing/TestDeviceService.kt
@@ -7,23 +7,36 @@ import com.greybox.projectmesh.messaging.data.entities.Message
import kotlinx.coroutines.runBlocking
import java.net.InetAddress
+/**
+ * Service for managing test devices in the mesh network.
+ *
+ * Handles initialization of both online and offline test devices,
+ * provides utility methods for identifying test devices, and
+ * generating echo responses for testing purposes.
+ */
class TestDeviceService {
companion object {
+ /** IP and name for the online test device */
const val TEST_DEVICE_IP = "192.168.0.99"
const val TEST_DEVICE_NAME = "Test Echo Device (Online)"
+
+ /** IP and name for the offline test device */
const val TEST_DEVICE_IP_OFFLINE = "192.168.0.98"
const val TEST_DEVICE_NAME_OFFLINE = "Test Echo Device (Offline)"
private var isInitialized = false
private var offlineDeviceInitialized = false
+ /**
+ * Initializes the online test device if it hasn't been set up already.
+ */
fun initialize() {
try {
if (!isInitialized) {
runBlocking {
val existingUser = userRepository.getUserByIp(TEST_DEVICE_IP)
if (existingUser == null) {
- // If there's no user with this IP, insert one with a "temp" UUID
+ // Insert a new user with a temporary UUID
val pseudoUuid = "temp-$TEST_DEVICE_IP"
userRepository.insertOrUpdateUser(
uuid = pseudoUuid,
@@ -31,8 +44,7 @@ class TestDeviceService {
address = TEST_DEVICE_IP
)
} else {
- // If a user with this IP already exists, just update the name
- // (keeping the same uuid and address)
+ // Update the name of an existing user with this IP
userRepository.insertOrUpdateUser(
uuid = existingUser.uuid,
name = TEST_DEVICE_NAME,
@@ -43,7 +55,7 @@ class TestDeviceService {
isInitialized = true
Log.d("TestDeviceService", "Test device initialized successfully with IP: $TEST_DEVICE_IP")
- //initialize offline test device
+ // Initialize the offline test device
initializeOfflineDevice()
}
} catch (e: Exception) {
@@ -51,25 +63,28 @@ class TestDeviceService {
}
}
+ /**
+ * Initializes the offline test device if it hasn't been set up already.
+ */
fun initializeOfflineDevice() {
try {
if (!offlineDeviceInitialized) {
runBlocking {
val existingUser = userRepository.getUserByIp(TEST_DEVICE_IP_OFFLINE)
if (existingUser == null) {
- // Create a new offline test device
+ // Create a new offline test device with null address
val pseudoUuid = "temp-offline-$TEST_DEVICE_IP_OFFLINE"
userRepository.insertOrUpdateUser(
uuid = pseudoUuid,
name = TEST_DEVICE_NAME_OFFLINE,
- address = null // NULL address means offline
+ address = null // null address indicates offline
)
} else {
- // Update existing offline device
+ // Update existing offline device to ensure it remains offline
userRepository.insertOrUpdateUser(
uuid = existingUser.uuid,
name = TEST_DEVICE_NAME_OFFLINE,
- address = null // Make sure it's offline
+ address = null
)
}
}
@@ -81,23 +96,32 @@ class TestDeviceService {
}
}
+ /** Checks if the given address is the online test device */
fun isOnlineTestDevice(address: InetAddress): Boolean {
return address.hostAddress == TEST_DEVICE_IP
}
+ /** Checks if the given address is the offline test device */
fun isOfflineTestDevice(address: InetAddress): Boolean {
return address.hostAddress == TEST_DEVICE_IP_OFFLINE
}
-
+ /** Returns the InetAddress of the online test device */
fun getTestDeviceAddress(): InetAddress {
return InetAddress.getByName(TEST_DEVICE_IP)
}
+ /** Checks if the given address matches the online test device */
fun isTestDevice(address: InetAddress): Boolean {
return address.hostAddress == TEST_DEVICE_IP
}
+ /**
+ * Generates an echo response message for testing purposes.
+ *
+ * @param originalMessage the original message to echo
+ * @return a new Message object containing the echo content
+ */
fun createEchoResponse(originalMessage: Message): Message {
return Message(
id = 0,
@@ -108,4 +132,4 @@ class TestDeviceService {
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/testing/TestMNetLogger.kt b/app/src/main/java/com/greybox/projectmesh/testing/TestMNetLogger.kt
index 2aabc3bef..98a59c65a 100644
--- a/app/src/main/java/com/greybox/projectmesh/testing/TestMNetLogger.kt
+++ b/app/src/main/java/com/greybox/projectmesh/testing/TestMNetLogger.kt
@@ -3,7 +3,21 @@ package com.greybox.projectmesh.testing
import android.util.Log
import com.ustadmobile.meshrabiya.log.MNetLogger
+/**
+ * Logger implementation for test devices.
+ *
+ * Redirects all log messages to Android's Log system with a fixed "TestDevice" tag.
+ * Supports both direct message strings and lambda message providers.
+ */
class TestMNetLogger : MNetLogger() {
+
+ /**
+ * Logs a message and optional exception with a given priority.
+ *
+ * @param priority the log priority (Log.VERBOSE, Log.DEBUG, etc.)
+ * @param message the message to log
+ * @param exception optional exception to log
+ */
override fun invoke(priority: Int, message: String, exception: Exception?) {
Log.println(priority, "TestDevice", message)
exception?.let {
@@ -11,10 +25,17 @@ class TestMNetLogger : MNetLogger() {
}
}
+ /**
+ * Logs a lazily evaluated message and optional exception with a given priority.
+ *
+ * @param priority the log priority (Log.VERBOSE, Log.DEBUG, etc.)
+ * @param message lambda returning the message to log
+ * @param exception optional exception to log
+ */
override fun invoke(priority: Int, message: () -> String, exception: Exception?) {
Log.println(priority, "TestDevice", message())
exception?.let {
Log.println(priority, "TestDevice", it.toString())
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/testing/TestVirtualRouter.kt b/app/src/main/java/com/greybox/projectmesh/testing/TestVirtualRouter.kt
index 3b843098f..19c2ef7cf 100644
--- a/app/src/main/java/com/greybox/projectmesh/testing/TestVirtualRouter.kt
+++ b/app/src/main/java/com/greybox/projectmesh/testing/TestVirtualRouter.kt
@@ -7,32 +7,70 @@ import com.ustadmobile.meshrabiya.vnet.socket.ChainSocketNextHop
import java.net.DatagramPacket
import java.net.InetAddress
+/**
+ * Test implementation of a VirtualRouter for use with test devices.
+ *
+ * Provides dummy routing behavior and predictable network parameters for testing purposes.
+ */
class TestVirtualRouter : VirtualRouter {
+
+ /** Fixed test device address */
override val address: InetAddress = InetAddress.getByName(TestDeviceService.TEST_DEVICE_IP)
+
+ /** Fixed port for local datagram operations */
override val localDatagramPort: Int = 4242
+
+ /** Fixed network prefix length for testing */
override val networkPrefixLength: Int = 16
- override fun route(packet: VirtualPacket, datagramPacket: DatagramPacket?, virtualNodeDatagramSocket: com.ustadmobile.meshrabiya.vnet.VirtualNodeDatagramSocket?) {
+ /**
+ * Route a packet.
+ *
+ * No-op implementation for test purposes.
+ */
+ override fun route(
+ packet: VirtualPacket,
+ datagramPacket: DatagramPacket?,
+ virtualNodeDatagramSocket: com.ustadmobile.meshrabiya.vnet.VirtualNodeDatagramSocket?
+ ) {
// no-op for test implementation
}
- override fun allocateUdpPortOrThrow(virtualDatagramSocketImpl: com.ustadmobile.meshrabiya.vnet.datagram.VirtualDatagramSocketImpl, portNum: Int): Int {
+ /**
+ * Allocate a UDP port or throw exception if unavailable.
+ *
+ * Always returns the requested port number in the test implementation.
+ */
+ override fun allocateUdpPortOrThrow(
+ virtualDatagramSocketImpl: com.ustadmobile.meshrabiya.vnet.datagram.VirtualDatagramSocketImpl,
+ portNum: Int
+ ): Int {
return portNum
}
+ /**
+ * Deallocate a port.
+ *
+ * No-op implementation for test purposes.
+ */
override fun deallocatePort(protocol: Protocol, portNum: Int) {
// no-op for test implementation
}
+ /**
+ * Look up the next hop for a chain socket.
+ *
+ * Returns a dummy next hop with isFinalDest = true for testing.
+ */
override fun lookupNextHopForChainSocket(address: InetAddress, port: Int): ChainSocketNextHop {
- //Return dummy next hop for testing with all required parameters
return ChainSocketNextHop(
address = address,
port = port,
isFinalDest = true,
- network = null //For testing purposes, we can pass null for the network
+ network = null // network is null for test purposes
)
}
+ /** Returns a constant MMCP message ID for testing */
override fun nextMmcpMessageId(): Int = 1
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/greybox/projectmesh/ui/theme/CustomButton.kt b/app/src/main/java/com/greybox/projectmesh/ui/theme/CustomButton.kt
index f08814f39..6a7c05451 100644
--- a/app/src/main/java/com/greybox/projectmesh/ui/theme/CustomButton.kt
+++ b/app/src/main/java/com/greybox/projectmesh/ui/theme/CustomButton.kt
@@ -27,7 +27,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
-// This is a pre-defined button with white background and black text
+/**
+ * A transparent button with white background and black text.
+ * Optional rounded corners and full-width by default.
+ */
@Composable
fun TransparentButton(
onClick: () -> Unit,
@@ -38,35 +41,46 @@ fun TransparentButton(
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
- containerColor = Color.White, // Background color
- contentColor = Color.Black // Text color
+ containerColor = Color.White, // White background
+ contentColor = Color.Black // Black text
),
border = BorderStroke(1.dp, Color.Black), // Black border
- shape = RoundedCornerShape(8.dp), // Optional: Rounded corners
- modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(8.dp), // Rounded corners
+ modifier = modifier.fillMaxWidth(), // Fill max width by default
enabled = enabled
) {
Text(text = text)
}
}
+/**
+ * A gradient button with press animation.
+ *
+ * @param text The label for the button.
+ * @param gradientColors Colors to use for horizontal gradient background.
+ * @param textColor Color of the text.
+ * @param maxWidth Maximum width of the button.
+ * @param onClick Action to perform when the button is clicked.
+ */
@Composable
fun GradientButton(
text: String,
modifier: Modifier = Modifier,
- gradientColors: List = listOf(Color(0xFF4CAF50), Color(0xFF81C784)), // Default gradient colors
+ gradientColors: List = listOf(Color(0xFF4CAF50), Color(0xFF81C784)), // Default gradient
textColor: Color = Color.White,
maxWidth: Dp = 120.dp,
onClick: () -> Unit
) {
var isPressed by remember { mutableStateOf(false) }
- val scale by animateFloatAsState(if (isPressed) 0.85f else 1f) // Scale down when pressed
+ val scale by animateFloatAsState(if (isPressed) 0.85f else 1f) // Scale down on press
+
LaunchedEffect(isPressed) {
if (isPressed) {
- delay(100) // Wait for 100 ms
+ delay(100) // Short delay to show pressed effect
isPressed = false
}
}
+
Box(
modifier = modifier
.scale(scale)
@@ -75,14 +89,14 @@ fun GradientButton(
brush = Brush.horizontalGradient(gradientColors),
shape = RoundedCornerShape(12.dp)
)
- .height(50.dp) // Height of the button
- .widthIn(min = 120.dp, max = maxWidth) // Width of the button
+ .height(50.dp)
+ .widthIn(min = 120.dp, max = maxWidth)
.padding(horizontal = 16.dp)
.clickable {
isPressed = true
onClick()
},
- contentAlignment = Alignment.Center // Center content in the box
+ contentAlignment = Alignment.Center // Center the text
) {
Text(
text = text,
@@ -90,42 +104,49 @@ fun GradientButton(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
- overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, // Truncate text if it overflows
+ overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
}
+/**
+ * A full-width gradient button with press animation.
+ *
+ * Similar to GradientButton but fills the available width.
+ */
@Composable
fun GradientLongButton(
text: String,
modifier: Modifier = Modifier,
- gradientColors: List = listOf(Color(0xFF4CAF50), Color(0xFF81C784)), // Default gradient colors
+ gradientColors: List = listOf(Color(0xFF4CAF50), Color(0xFF81C784)), // Default gradient
textColor: Color = Color.White,
onClick: () -> Unit
) {
var isPressed by remember { mutableStateOf(false) }
- val scale by animateFloatAsState(if (isPressed) 0.85f else 1f) // Scale down when pressed
+ val scale by animateFloatAsState(if (isPressed) 0.85f else 1f) // Scale down on press
+
LaunchedEffect(isPressed) {
if (isPressed) {
- delay(100) // Wait for 100 ms
+ delay(100)
isPressed = false
}
}
+
Box(
modifier = modifier
- .fillMaxWidth()
+ .fillMaxWidth() // Fill the full width
.scale(scale)
- .shadow(8.dp, RoundedCornerShape(12.dp)) // Shadow effect
+ .shadow(8.dp, RoundedCornerShape(12.dp))
.background(
brush = Brush.horizontalGradient(gradientColors),
shape = RoundedCornerShape(12.dp)
)
- .height(50.dp) // Height of the button
+ .height(50.dp)
.clickable {
isPressed = true
onClick()
},
- contentAlignment = Alignment.Center // Center content in the box
+ contentAlignment = Alignment.Center
) {
Text(
text = text,
@@ -133,7 +154,7 @@ fun GradientLongButton(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
- overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, // Truncate text if it overflows
+ overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
}
diff --git a/app/src/main/java/com/greybox/projectmesh/ui/theme/Theme.kt b/app/src/main/java/com/greybox/projectmesh/ui/theme/Theme.kt
index 3232e0b31..680c8a13a 100644
--- a/app/src/main/java/com/greybox/projectmesh/ui/theme/Theme.kt
+++ b/app/src/main/java/com/greybox/projectmesh/ui/theme/Theme.kt
@@ -5,7 +5,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.material3.*
import androidx.compose.ui.graphics.Color
-// Define the color schemes for light and dark themes
+/**
+ * Dark color scheme for the app.
+ */
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFFBB86FC),
secondary = Color(0xFF03DAC5),
@@ -17,6 +19,9 @@ private val DarkColorScheme = darkColorScheme(
onSurface = Color.White
)
+/**
+ * Light color scheme for the app.
+ */
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF6200EE),
secondary = Color(0xFF03DAC5),
@@ -28,23 +33,33 @@ private val LightColorScheme = lightColorScheme(
onSurface = Color.Black
)
+/**
+ * Enum to represent the app's theme choice.
+ */
enum class AppTheme {
SYSTEM, LIGHT, DARK
}
+/**
+ * Apply ProjectMesh theme with the chosen AppTheme.
+ *
+ * @param appTheme The selected theme (System, Light, Dark)
+ * @param content The composable content to wrap with this theme
+ */
@Composable
fun ProjectMeshTheme(
appTheme: AppTheme,
content: @Composable () -> Unit
) {
val darkTheme = when (appTheme) {
- AppTheme.SYSTEM -> isSystemInDarkTheme()
+ AppTheme.SYSTEM -> isSystemInDarkTheme() // Follow system setting
AppTheme.LIGHT -> false
AppTheme.DARK -> true
}
+
MaterialTheme(
colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme,
- content = content,
- typography = Typography,
+ typography = Typography, // Apply predefined typography
+ content = content
)
}
diff --git a/app/src/main/java/com/greybox/projectmesh/user/README.md b/app/src/main/java/com/greybox/projectmesh/user/README.md
index 4b7a58b6d..b9ac34ebe 100644
--- a/app/src/main/java/com/greybox/projectmesh/user/README.md
+++ b/app/src/main/java/com/greybox/projectmesh/user/README.md
@@ -1,12 +1,16 @@
# User Profiles in Project Mesh
## Overview
+
Project Mesh implements a user profile system that allows devices to identify themselves on the mesh network. User profiles consist of a unique identifier (UUID), a display name, and network address information. This system enables personalized messaging and device identification across the mesh network.
+
## Key Components
+
### User Entity
+
The core of the user profile system is the UserEntity class, which stores all user data:
-```kotlin
+```kotlin
// In UserEntity.kt
@Serializable
@Entity(tableName = "users")
@@ -18,9 +22,11 @@ data class UserEntity(
)
```
-### User Repository
+### User Repository
+
The UserRepository manages all database operations related to user profiles:
-```kotlin
+
+```kotlin
// In UserRepository.kt
class UserRepository(private val userDao: UserDao) {
@@ -49,8 +55,11 @@ class UserRepository(private val userDao: UserDao) {
// Other repository methods...
}
```
+
### UserData Access Object (DAO)
+
The UserDao interface defines database operations:
+
```kotlin
// In UserDao.kt
@Dao
@@ -79,12 +88,14 @@ interface UserDao {
```
## User Profile Lifecycle
+
### First-time Setup
When a user first launches the app, they go through an onboarding process to set up their profile:
-```kotlin
+
+```kotlin
// In OnboardingViewModel.kt
fun handleFirstTimeSetup(onComplete: () -> Unit) {
viewModelScope.launch {
@@ -108,6 +119,7 @@ fun handleFirstTimeSetup(onComplete: () -> Unit) {
```
### User Information Exchange
+
When devices connect, they exchange user information:
**Before Name Exchange**:
@@ -117,11 +129,11 @@ When devices connect, they exchange user information:
-```kotlin
+```kotlin
// In AppServer.kt - requesting user info
fun requestRemoteUserInfo(remoteAddr: InetAddress, port: Int = DEFAULT_PORT) {
// Special handling for test devices...
-
+
scope.launch {
try {
val url = "http://${remoteAddr.hostAddress}:$port/myinfo"
@@ -130,7 +142,7 @@ fun requestRemoteUserInfo(remoteAddr: InetAddress, port: Int = DEFAULT_PORT) {
val response = httpClient.newCall(request).execute()
val userJson = response.body?.string()
-
+
if (!userJson.isNullOrEmpty()) {
// Decode JSON
val remoteUser = json.decodeFromString(UserEntity.serializer(), userJson)
@@ -141,7 +153,7 @@ fun requestRemoteUserInfo(remoteAddr: InetAddress, port: Int = DEFAULT_PORT) {
remoteUserWithIp.name,
remoteUserWithIp.address
)
-
+
// Update user status...
}
} catch (e: Exception) {
@@ -172,16 +184,17 @@ private fun handleMyInfoRequest(): Response {
```
### Profile Updates
+
Users can update their profile information in the Settings screen:
-*Found in Settings Under Network > Device Name*:
+_Found in Settings Under Network > Device Name_:
-*User name can be Updated* :
+_User name can be Updated_ :
-```kotlin
+```kotlin
// In SettingsScreen.kt
onDeviceNameChange = { newDeviceName ->
Log.d("BottomNavApp", "Device name changed to: $newDeviceName")
@@ -197,7 +210,7 @@ onDeviceNameChange = { newDeviceName ->
name = newDeviceName,
address = appServer.localVirtualAddr.hostAddress
)
-
+
// 2. Broadcast updated name to connected users
val connectedUsers = userRepository.getAllConnectedUsers()
connectedUsers.forEach { user ->
@@ -216,17 +229,19 @@ onDeviceNameChange = { newDeviceName ->
```
### Online Status Tracking
+
The application tracks which users are online using the DeviceStatusManager:
-```kotlin
+
+```kotlin
// In DeviceStatusManager.kt
object DeviceStatusManager {
private val _deviceStatusMap = MutableStateFlow