diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 7643783..e28030c 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,5 +1,6 @@
+
@@ -120,4 +121,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
deleted file mode 100644
index 2319ac6..0000000
--- a/.idea/deploymentTargetDropDown.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 120708d..2c2f93b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -22,8 +22,8 @@ android {
applicationId "com.cstef.meshlink"
minSdk 26
targetSdk 33
- versionCode 29
- versionName "1.2.2"
+ versionCode 30
+ versionName "1.2.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -44,7 +44,7 @@ android {
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
- implementation 'androidx.appcompat:appcompat:1.5.1'
+ implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation('com.daveanthonythomas.moshipack:moshipack:1.0.1') {
@@ -53,40 +53,47 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:1.4.10"
implementation "androidx.compose.compiler:compiler:$compose_version"
- implementation "androidx.compose.runtime:runtime:1.3.2"
- implementation 'androidx.compose.material3:material3:1.1.0-alpha03'
+ implementation "androidx.compose.runtime:runtime:1.3.3"
+ implementation 'androidx.compose.material3:material3:1.1.0-alpha04'
- implementation 'androidx.compose.ui:ui-tooling-preview:1.4.0-alpha03'
- debugImplementation 'androidx.compose.ui:ui-tooling:1.4.0-alpha03'
+ implementation 'androidx.compose.ui:ui-tooling-preview:1.4.0-alpha04'
+ debugImplementation 'androidx.compose.ui:ui-tooling:1.4.0-alpha04'
implementation 'androidx.activity:activity-compose:1.6.1'
implementation 'com.google.accompanist:accompanist-systemuicontroller:0.28.0'
implementation "com.google.accompanist:accompanist-permissions:0.28.0"
// material-icons-extended
- implementation 'androidx.compose.material:material-icons-extended:1.4.0-alpha03'
+ implementation 'androidx.compose.material:material-icons-extended:1.4.0-alpha04'
// Navigation
implementation 'androidx.navigation:navigation-compose:2.6.0-alpha04'
implementation "com.google.accompanist:accompanist-navigation-animation:0.28.0"
//AndroidSVG
implementation 'com.caverock:androidsvg-aar:1.4'
- def room_version = "2.4.3"
+ // Room
+ def room_version = "2.5.0"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
-
kapt "androidx.room:room-compiler:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
implementation "androidx.room:room-ktx:$room_version"
- implementation "androidx.compose.runtime:runtime-livedata:1.3.2"
+ implementation "androidx.compose.runtime:runtime-livedata:1.3.3"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
+ // Database
implementation "net.zetetic:android-database-sqlcipher:4.5.2"
- implementation "androidx.sqlite:sqlite:2.2.0"
+ implementation "androidx.sqlite:sqlite:2.3.0"
+ // QR Code
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
+ // Easter egg
implementation 'nl.dionsegijn:konfetti-compose:2.0.2'
+
+ // Charts
+ implementation "com.patrykandpatrick.vico:compose:1.6.2"
+ implementation "com.patrykandpatrick.vico:compose-m3:1.6.2"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 47c3254..9373e78 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -33,7 +33,6 @@
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Meshlinkkotlin">
>
+ get() = logsManager.logcatMessages
val isAdvertising
get() = bleManager.isAdvertising
val service: BleService
@@ -59,10 +63,15 @@ class BleService : Service() {
bleManager.setUserId(id)
}
- fun sendMessage(recipientId: String, message: String, type: String = Message.Type.TEXT) {
+ fun sendMessage(
+ recipientId: String,
+ message: String,
+ type: String = Message.Type.TEXT
+ ): String {
+ val id = UUID.randomUUID().toString()
this@BleService.sendMessage(
Message(
- UUID.randomUUID().toString(),
+ id,
userId,
recipientId,
message,
@@ -71,6 +80,7 @@ class BleService : Service() {
true
)
)
+ return id
}
fun addDevice(userId: String) {
@@ -177,6 +187,7 @@ class BleService : Service() {
val messagesHashes: MutableMap> = mutableMapOf()
val chunks: MutableMap>> =
mutableMapOf() // userId -> messageId -> chunks
+ val sentMessages: MutableMap> = mutableMapOf()
private val bleDataExchangeManager = object : BleManager.BleDataExchangeManager {
override fun connect(address: String) {
@@ -218,7 +229,8 @@ class BleService : Service() {
name = null,
blocked = false,
publicKey = publicKey?.encoded?.let { Base64.encodeToString(it, Base64.DEFAULT) },
- added = false
+ added = false,
+ txPower = 0
)
)
} else {
@@ -241,10 +253,15 @@ class BleService : Service() {
}
}
- override fun onMessageSent(userId: String) {
+ override fun onMessageSent(userId: String, messageId: String) {
val sharedPreferences = getSharedPreferences("USER_STATS", MODE_PRIVATE)
- val sentMessages = sharedPreferences.getInt("TOTAL_MESSAGES_DELIVERED", 0)
- sharedPreferences.edit().putInt("TOTAL_MESSAGES_DELIVERED", sentMessages + 1).apply()
+ val deliveredMessages = sharedPreferences.getInt("TOTAL_MESSAGES_DELIVERED", 0)
+ sharedPreferences.edit().putInt("TOTAL_MESSAGES_DELIVERED", deliveredMessages + 1).apply()
+ // Add the message to the sent messages list
+ val sentMessagesForUser = sentMessages[userId] ?: mutableListOf()
+ sentMessagesForUser.add(messageId)
+ sentMessages[userId] = sentMessagesForUser
+ // Remove the message from the chunks list
Log.d("BleService", "onMessageSent: $userId")
}
@@ -258,9 +275,11 @@ class BleService : Service() {
if (!bleManager.isConnected(address) && !bleManager.isConnecting(address)) {
bleManager.connect(address)
}
- // is user blocked?
val devices = allDevices?.value ?: emptyList()
val device = devices.find { it.userId == getUserIdForAddress(address) }
+ if (device?.connected == false && bleManager.isConnected(address)) {
+ deviceRepository?.update(device.copy(connected = true))
+ }
if (device != null && device.blocked) {
Log.d("BleService", "onChunkReceived: user is blocked")
return
@@ -387,7 +406,18 @@ class BleService : Service() {
if (devices.isNotEmpty()) {
devices.find { it.userId == userId }?.let { device ->
if (device.rssi != rssi) {
- deviceRepository?.update(device.copy(rssi = rssi))
+ deviceRepository?.update(device.copy(rssi = rssi, connected = true))
+ }
+ }
+ }
+ }
+
+ override fun onUserTxPowerReceived(userId: String, txPower: Int) {
+ val devices = allDevices?.value ?: return
+ if (devices.isNotEmpty()) {
+ devices.find { it.userId == userId }?.let { device ->
+ if (device.txPower != txPower) {
+ deviceRepository?.update(device.copy(txPower = txPower, connected = true))
}
}
}
@@ -415,6 +445,7 @@ class BleService : Service() {
}
lateinit var bleManager: BleManager
private lateinit var encryptionManager: EncryptionManager
+ lateinit var logsManager: LogsManager
var allMessages: LiveData>? = null
var allDevices: LiveData>? = null
@@ -433,6 +464,7 @@ class BleService : Service() {
super.onCreate()
handlerThread.start()
handler = Handler(handlerThread.looper)
+ logsManager = LogsManager()
encryptionManager = EncryptionManager()
bleManager = BleManager(applicationContext, bleDataExchangeManager, encryptionManager, handler)
}
diff --git a/app/src/main/java/com/cstef/meshlink/MainActivity.kt b/app/src/main/java/com/cstef/meshlink/MainActivity.kt
index 0e14371..cd969fe 100644
--- a/app/src/main/java/com/cstef/meshlink/MainActivity.kt
+++ b/app/src/main/java/com/cstef/meshlink/MainActivity.kt
@@ -123,51 +123,39 @@ class MainActivity : AppCompatActivity() {
mutableStateOf(sharedPreferences.getBoolean("is_default_password", false))
}
AnimatedNavHost(navController = navController, startDestination = "home") {
- composable(
- "home",
- enterTransition = {
- if (!isDatabaseOpening) {
- slideInHorizontally(
- initialOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- } else {
- fadeIn(animationSpec = tween(300))
- }
- },
- exitTransition = {
- if (!isDatabaseOpening) {
- slideOutHorizontally(
- targetOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- } else {
- null
- }
- },
- popEnterTransition = {
+ composable("home", enterTransition = {
+ if (!isDatabaseOpening) {
slideInHorizontally(
- initialOffsetX = { -1000 },
- animationSpec = tween(300)
+ initialOffsetX = { 1000 }, animationSpec = tween(300)
) + fadeIn(animationSpec = tween(300))
- },
- popExitTransition = {
+ } else {
+ fadeIn(animationSpec = tween(300))
+ }
+ }, exitTransition = {
+ if (!isDatabaseOpening) {
slideOutHorizontally(
- targetOffsetX = { 1000 },
- animationSpec = tween(300)
+ targetOffsetX = { -1000 }, animationSpec = tween(300)
) + fadeOut(animationSpec = tween(300))
+ } else {
+ null
}
- ) {
+ }, popEnterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, popExitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }) {
if (isDatabaseOpen) {
Box(modifier = Modifier.fillMaxSize()) {
bleBinder?.let { binder ->
- HomeScreen(
- allDevices = binder.allDevices,
+ HomeScreen(allDevices = binder.allDevices,
userId = userId,
onSelfClick = { navController.navigate("user/$userId") },
onDeviceLongClick = { navController.navigate("user/$it") },
- onDeviceSelected = { navController.navigate("chat/$it") }
- )
+ onDeviceSelected = { navController.navigate("chat/$it") })
FloatingActionButton(
onClick = { navController.navigate("broadcast") },
modifier = Modifier
@@ -183,8 +171,7 @@ class MainActivity : AppCompatActivity() {
FloatingActionButton(
onClick = {
navController.navigate("add")
- },
- modifier = Modifier
+ }, modifier = Modifier
.align(Alignment.BottomEnd)
.padding(24.dp)
) {
@@ -214,38 +201,31 @@ class MainActivity : AppCompatActivity() {
}
}
}
- composable(
- "chat/{deviceId}",
+ composable("chat/{deviceId}",
arguments = listOf(navArgument("deviceId") { type = NavType.StringType }),
enterTransition = {
slideInHorizontally(
- initialOffsetX = { 1000 },
- animationSpec = tween(300)
+ initialOffsetX = { 1000 }, animationSpec = tween(300)
) + fadeIn(animationSpec = tween(300))
},
exitTransition = {
slideOutHorizontally(
- targetOffsetX = { -1000 },
- animationSpec = tween(300)
+ targetOffsetX = { -1000 }, animationSpec = tween(300)
) + fadeOut(animationSpec = tween(300))
},
popEnterTransition = {
slideInHorizontally(
- initialOffsetX = { -1000 },
- animationSpec = tween(300)
+ initialOffsetX = { -1000 }, animationSpec = tween(300)
) + fadeIn(animationSpec = tween(300))
},
popExitTransition = {
slideOutHorizontally(
- targetOffsetX = { 1000 },
- animationSpec = tween(300)
+ targetOffsetX = { 1000 }, animationSpec = tween(300)
) + fadeOut(animationSpec = tween(300))
- }
- ) { backStackEntry ->
+ }) { backStackEntry ->
bleBinder?.let { binder ->
val deviceId = backStackEntry.arguments?.getString("deviceId") ?: ""
- ChatScreen(
- deviceId = deviceId,
+ ChatScreen(deviceId = deviceId,
allMessages = binder.allMessages,
allDevices = binder.allDevices,
sendMessage = { content, type ->
@@ -256,77 +236,57 @@ class MainActivity : AppCompatActivity() {
})
}
}
- composable(
- "add",
- enterTransition = {
- slideInHorizontally(
- initialOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- exitTransition = {
- slideOutHorizontally(
- targetOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- },
- popEnterTransition = {
- slideInHorizontally(
- initialOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- popExitTransition = {
- slideOutHorizontally(
- targetOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- }
- ) {
+ composable("add", enterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, exitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }, popEnterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, popExitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }) {
bleBinder?.let { binder ->
- AddDeviceScreen(
- allDevices = binder.allDevices,
- addDevice = { id ->
- binder.addDevice(id)
- },
- onBack = { user ->
- if (user != null) {
- navController.navigate("user/${user}")
- } else {
- navController.popBackStack()
- }
+ AddDeviceScreen(allDevices = binder.allDevices, addDevice = { id ->
+ binder.addDevice(id)
+ }, onBack = { user ->
+ if (user != null) {
+ navController.navigate("user/${user}")
+ } else {
+ navController.popBackStack()
}
- )
+ })
}
}
- composable(
- "user/{deviceId}",
+ composable("user/{deviceId}",
arguments = listOf(navArgument("deviceId") { type = NavType.StringType }),
enterTransition = {
slideInHorizontally(
- initialOffsetX = { 1000 },
- animationSpec = tween(300)
+ initialOffsetX = { 1000 }, animationSpec = tween(300)
) + fadeIn(animationSpec = tween(300))
},
exitTransition = {
slideOutHorizontally(
- targetOffsetX = { -1000 },
- animationSpec = tween(300)
+ targetOffsetX = { -1000 }, animationSpec = tween(300)
) + fadeOut(animationSpec = tween(300))
},
popEnterTransition = {
slideInHorizontally(
- initialOffsetX = { -1000 },
- animationSpec = tween(300)
+ initialOffsetX = { -1000 }, animationSpec = tween(300)
) + fadeIn(animationSpec = tween(300))
},
popExitTransition = {
slideOutHorizontally(
- targetOffsetX = { 1000 },
- animationSpec = tween(300)
+ targetOffsetX = { 1000 }, animationSpec = tween(300)
) + fadeOut(animationSpec = tween(300))
- }
- ) { backStackEntry ->
+ }) { backStackEntry ->
// User info screen
val otherUserId = backStackEntry.arguments?.getString("deviceId") ?: ""
bleBinder?.let { binder ->
@@ -356,33 +316,23 @@ class MainActivity : AppCompatActivity() {
)
}
}
- composable(
- "settings",
- enterTransition = {
- slideInHorizontally(
- initialOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- exitTransition = {
- slideOutHorizontally(
- targetOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- },
- popEnterTransition = {
- slideInHorizontally(
- initialOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- popExitTransition = {
- slideOutHorizontally(
- targetOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- }
- ) {
+ composable("settings", enterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, exitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }, popEnterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, popExitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }) {
bleBinder?.let { binder ->
SettingsScreen(
isAdvertising = binder.isAdvertising,
@@ -398,6 +348,12 @@ class MainActivity : AppCompatActivity() {
goToStats = {
navController.navigate("stats")
},
+ goToLogs = {
+ navController.navigate("logs")
+ },
+ goToBenchmark = {
+ navController.navigate("benchmark")
+ },
deleteAllData = {
binder.deleteAllData()
},
@@ -407,62 +363,42 @@ class MainActivity : AppCompatActivity() {
)
}
}
- composable(
- "about",
- enterTransition = {
- slideInHorizontally(
- initialOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- exitTransition = {
- slideOutHorizontally(
- targetOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- },
- popEnterTransition = {
- slideInHorizontally(
- initialOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- popExitTransition = {
- slideOutHorizontally(
- targetOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- }
- ) {
+ composable("about", enterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, exitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }, popEnterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, popExitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }) {
AboutScreen()
}
- composable(
- "password",
- enterTransition = {
- slideInHorizontally(
- initialOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- exitTransition = {
- slideOutHorizontally(
- targetOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- },
- popEnterTransition = {
- slideInHorizontally(
- initialOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- popExitTransition = {
- slideOutHorizontally(
- targetOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- }
- ) {
+ composable("password", enterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, exitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }, popEnterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, popExitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }) {
bleBinder?.let { binder ->
PasswordScreen(
firstTime = firstTime,
@@ -473,32 +409,23 @@ class MainActivity : AppCompatActivity() {
)
}
}
- composable("broadcast",
- enterTransition = {
- slideInHorizontally(
- initialOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- exitTransition = {
- slideOutHorizontally(
- targetOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- },
- popEnterTransition = {
- slideInHorizontally(
- initialOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- popExitTransition = {
- slideOutHorizontally(
- targetOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- }
- ) {
+ composable("broadcast", enterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, exitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }, popEnterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, popExitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }) {
bleBinder?.let { binder ->
BroadcastScreen(
allMessages = binder.allMessages,
@@ -512,34 +439,73 @@ class MainActivity : AppCompatActivity() {
)
}
}
- composable("stats",
- enterTransition = {
- slideInHorizontally(
- initialOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- exitTransition = {
- slideOutHorizontally(
- targetOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- },
- popEnterTransition = {
- slideInHorizontally(
- initialOffsetX = { -1000 },
- animationSpec = tween(300)
- ) + fadeIn(animationSpec = tween(300))
- },
- popExitTransition = {
- slideOutHorizontally(
- targetOffsetX = { 1000 },
- animationSpec = tween(300)
- ) + fadeOut(animationSpec = tween(300))
- }
- ) {
+ composable("stats", enterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, exitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }, popEnterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, popExitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }) {
StatsScreen()
}
+ composable("logs", enterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, exitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }, popEnterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, popExitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }) {
+ bleBinder?.let { binder ->
+ LogsScreen(
+ logcatLogs = binder.logcatLogs,
+ ) {
+ binder.service.logsManager.logcatMessages.value = emptyList()
+ }
+ }
+ }
+ composable("benchmark", enterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, exitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }, popEnterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { -1000 }, animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300))
+ }, popExitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { 1000 }, animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ }) {
+ bleBinder?.let { binder ->
+ BenchmarkScreen(
+ binder = binder,
+ )
+ }
+ }
}
LaunchedEffect(databaseError) {
if (databaseError.isNotEmpty()) {
@@ -562,40 +528,36 @@ class MainActivity : AppCompatActivity() {
popUpTo("home") { inclusive = true }
}
}
- RequestMultiplePermissions(
- permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- listOf(
- Manifest.permission.ACCESS_FINE_LOCATION,
- Manifest.permission.ACCESS_COARSE_LOCATION,
- Manifest.permission.BLUETOOTH,
- Manifest.permission.BLUETOOTH_ADVERTISE,
- Manifest.permission.BLUETOOTH_CONNECT,
- Manifest.permission.BLUETOOTH_SCAN,
- )
- } else {
- listOf(
- Manifest.permission.ACCESS_FINE_LOCATION,
- Manifest.permission.ACCESS_COARSE_LOCATION,
- Manifest.permission.BLUETOOTH,
- Manifest.permission.BLUETOOTH_ADMIN,
- )
- },
- content = {
- LaunchedEffect(Unit) {
- if (sharedPreferences.getBoolean("first_time", true)) {
- val editor = sharedPreferences.edit()
- editor.putBoolean("first_time", false)
- editor.apply()
- }
- if (!checkBluetoothEnabled()) {
- Toast.makeText(this@MainActivity, "Bluetooth is disabled", Toast.LENGTH_LONG)
- .show()
- } else {
- start()
- }
+ RequestMultiplePermissions(permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ listOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.BLUETOOTH,
+ Manifest.permission.BLUETOOTH_ADVERTISE,
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_SCAN,
+ )
+ } else {
+ listOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.BLUETOOTH,
+ Manifest.permission.BLUETOOTH_ADMIN,
+ )
+ }, content = {
+ LaunchedEffect(Unit) {
+ if (sharedPreferences.getBoolean("first_time", true)) {
+ val editor = sharedPreferences.edit()
+ editor.putBoolean("first_time", false)
+ editor.apply()
+ }
+ if (!checkBluetoothEnabled()) {
+ Toast.makeText(this@MainActivity, "Bluetooth is disabled", Toast.LENGTH_LONG).show()
+ } else {
+ start()
}
}
- )
+ })
}
}
}
diff --git a/app/src/main/java/com/cstef/meshlink/db/Database.kt b/app/src/main/java/com/cstef/meshlink/db/Database.kt
index f05f149..3f42733 100644
--- a/app/src/main/java/com/cstef/meshlink/db/Database.kt
+++ b/app/src/main/java/com/cstef/meshlink/db/Database.kt
@@ -16,7 +16,7 @@ import net.sqlcipher.database.SupportFactory
Device::class,
Message::class,
],
- version = 16,
+ version = 17,
exportSchema = false,
)
abstract class AppDatabase : RoomDatabase() {
diff --git a/app/src/main/java/com/cstef/meshlink/db/entities/Device.kt b/app/src/main/java/com/cstef/meshlink/db/entities/Device.kt
index 195e7db..8d2a3b4 100644
--- a/app/src/main/java/com/cstef/meshlink/db/entities/Device.kt
+++ b/app/src/main/java/com/cstef/meshlink/db/entities/Device.kt
@@ -15,4 +15,5 @@ data class Device(
@ColumnInfo(name = "blocked") val blocked: Boolean = false,
@ColumnInfo(name = "public_key") val publicKey: String? = null,
@ColumnInfo(name = "added") val added: Boolean = false,
+ @ColumnInfo(name = "tx_power") val txPower: Int = 0,
)
diff --git a/app/src/main/java/com/cstef/meshlink/managers/BleManager.kt b/app/src/main/java/com/cstef/meshlink/managers/BleManager.kt
index 7149e78..75079c6 100644
--- a/app/src/main/java/com/cstef/meshlink/managers/BleManager.kt
+++ b/app/src/main/java/com/cstef/meshlink/managers/BleManager.kt
@@ -31,8 +31,6 @@ class BleManager(
val isAdvertising = mutableStateOf(false)
val isStarted = mutableStateOf(false)
- val isScanning = mutableStateOf(false)
- private val tag = BleManager::class.java.canonicalName
private val adapter get() = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
// BLE Callbacks are executed on a binder thread. This handlers gets the work
@@ -61,7 +59,7 @@ class BleManager(
@SuppressLint("MissingPermission")
fun stop() {
- Log.d(tag, "BleManager stopped")
+ Log.d("BleManager", "BleManager stopped")
if (canBeClient) clientManager.stop()
if (canBeServer) serverManager.stopAdvertising()
isStarted.value = false
@@ -70,15 +68,15 @@ class BleManager(
fun sendMessage(message: Message) {
val deviceAddress = clientManager.connectedServersAddresses[message.recipientId]
if (deviceAddress != null && clientManager.connectedGattServers.containsKey(deviceAddress)) {
- Log.d(tag, clientManager.connectedGattServers[deviceAddress]?.device?.address!!)
- Log.d(tag, "Sending data to ${message.recipientId}")
+ Log.d("BleManager", clientManager.connectedGattServers[deviceAddress]?.device?.address!!)
+ Log.d("BleManager", "Sending data to ${message.recipientId}")
clientManager.sendData(message)
} else {
- Log.d(tag, "No connection to ${message.recipientId}, broadcasting data")
+ Log.d("BleManager", "No connection to ${message.recipientId}, broadcasting data")
if (message.type == Message.Type.TEXT) {
clientManager.broadcastData(message)
} else {
- Log.e(tag, "Cannot broadcast other than text messages")
+ Log.e("BleManager", "Cannot broadcast other than text messages")
}
}
}
@@ -87,13 +85,13 @@ class BleManager(
if (message.type == Message.Type.TEXT) {
clientManager.broadcastData(message)
} else {
- Log.e(tag, "Cannot broadcast other than text messages")
+ Log.e("BleManager", "Cannot broadcast other than text messages")
}
}
fun getUserIdForAddress(address: String): String? {
Log.d(
- tag,
+ "BleManager",
"Getting user id for address $address, connected servers: ${clientManager.connectedServersAddresses}"
)
return clientManager.connectedServersAddresses.entries.find { it.value == address }?.key
@@ -174,7 +172,7 @@ class BleManager(
fun getUsername(): String = ""
fun getPublicKeyForUser(recipientId: String): PublicKey?
fun onUserRssiReceived(userId: String, rssi: Int) {}
- fun onMessageSent(userId: String) {}
+ fun onMessageSent(userId: String, messageId: String) {}
fun onUserWriting(userId: String, isWriting: Boolean) {}
fun getUserIdForAddress(address: String): String? = ""
fun onMessageSendFailed(userId: String?, reason: String?) {}
@@ -182,5 +180,6 @@ class BleManager(
fun getAddressForUserId(userId: String): String = ""
fun onUserAdded(userId: String)
fun connect(address: String) {}
+ fun onUserTxPowerReceived(userId: String, txPower: Int) {}
}
}
diff --git a/app/src/main/java/com/cstef/meshlink/managers/ClientBleManager.kt b/app/src/main/java/com/cstef/meshlink/managers/ClientBleManager.kt
index a4d7f95..cb2cdda 100644
--- a/app/src/main/java/com/cstef/meshlink/managers/ClientBleManager.kt
+++ b/app/src/main/java/com/cstef/meshlink/managers/ClientBleManager.kt
@@ -63,7 +63,8 @@ class ClientBleManager(
val userId: String,
val notificationId: Int,
val startTime: Long = System.currentTimeMillis(),
- val receiver: BroadcastReceiver
+ val receiver: BroadcastReceiver,
+ val messageId: String
)
private val sendingChunks = mutableMapOf()
@@ -72,10 +73,14 @@ class ClientBleManager(
private val scanner get() = adapter?.bluetoothLeScanner
private val scanFilters =
- ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(BleUuid.SERVICE_UUID)).build()
+ ScanFilter.Builder()
+ .setServiceUuid(ParcelUuid.fromString(BleUuid.SERVICE_UUID))
+ // .setManufacturerData(0xDEAD, byteArrayOf(0x00))
+ .build()
.let { listOf(it) }
- private val scanSettings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
+ private val scanSettings = ScanSettings.Builder()
+ .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE).build()
@@ -94,6 +99,18 @@ class ClientBleManager(
}
}
}
+ result.txPower.let { txPower ->
+ if (txPower == ScanResult.TX_POWER_NOT_PRESENT) {
+ Log.d("ClientBleManager", "onScanResult: txPower not present")
+ return@execute
+ }
+ connectedServersAddresses.entries.find { it.value == device.address }?.key.let { userId ->
+// Log.d("ClientBleManager", "onScanResult: $userId $txPower")
+ if (userId != null) {
+ dataExchangeManager.onUserTxPowerReceived(userId, txPower)
+ }
+ }
+ }
operationQueue.operationComplete()
return@execute
} else {
@@ -180,6 +197,16 @@ class ClientBleManager(
val serverId =
connectedServersAddresses.entries.find { it.value == gatt.device.address }?.key
connectedServersAddresses.remove(serverId)
+ if (sendingChunks.containsKey(gatt.device.address)) {
+ sendingChunks.remove(gatt.device.address)
+ val chunkSendingState = sendingChunks[gatt.device.address]
+ val notificationId = chunkSendingState?.notificationId ?: serverId.hashCode()
+ NotificationManagerCompat.from(context).cancel(notificationId)
+ dataExchangeManager.onMessageSendFailed(serverId, "Connection error")
+ }
+ if (serverId != null) {
+ dataExchangeManager.onUserDisconnected(serverId)
+ }
gatt.close()
}
}
@@ -257,12 +284,14 @@ class ClientBleManager(
.generatePublic(X509EncodedKeySpec(Base64.decode(msg.key, Base64.DEFAULT)))
Log.d(
"ClientBleManager",
- "onCharacteristicRead: publicKey = $publicKey gatt == null: ${gatt == null}"
+ "onCharacteristicRead: publicKey = ${encryptionManager.getPublicKeySignature(publicKey)} gatt == null: ${gatt == null}"
)
if (gatt != null) {
callbackHandler.post {
dataExchangeManager.onUserConnected(
- msg.userId, gatt.device.address, publicKey
+ userId = msg.userId,
+ address = gatt.device.address,
+ publicKey = publicKey
)
}
} else {
@@ -308,7 +337,7 @@ class ClientBleManager(
notificationManager.cancel(chunkState.notificationId)
sendingChunks.remove(gatt?.device?.address)
callbackHandler.post {
- dataExchangeManager.onMessageSent(chunkState.userId)
+ dataExchangeManager.onMessageSent(chunkState.userId, chunkState.messageId)
}
} else {
val timeDiff = System.currentTimeMillis() - chunkState.startTime
@@ -357,6 +386,7 @@ class ClientBleManager(
}
} else {
Log.e("ClientBleManager", "onReadRemoteRssi: userId == null: ${gatt?.device?.address}")
+ gatt?.disconnect()
}
} else {
Log.e("ClientBleManager", "onReadRemoteRssi: failed to read rssi")
@@ -455,7 +485,8 @@ class ClientBleManager(
userId = message.recipientId ?: "",
notificationId = notificationId,
startTime = System.currentTimeMillis(),
- receiver = receiver
+ receiver = receiver,
+ messageId = message.id
)
context.registerReceiver(receiver, IntentFilter("cancel_sending"))
chunks.forEachIndexed { index, chunk ->
diff --git a/app/src/main/java/com/cstef/meshlink/managers/LogsManager.kt b/app/src/main/java/com/cstef/meshlink/managers/LogsManager.kt
new file mode 100644
index 0000000..146ca93
--- /dev/null
+++ b/app/src/main/java/com/cstef/meshlink/managers/LogsManager.kt
@@ -0,0 +1,101 @@
+package com.cstef.meshlink.managers
+
+import android.icu.util.GregorianCalendar
+import androidx.lifecycle.MutableLiveData
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+// A class that listens to logcat on a separate thread and formats them into a LogcatMessage object
+
+data class LogcatMessage(
+ val pid: String,
+ val tid: String,
+ val priority: String,
+ val tag: String,
+ val message: String,
+ val time: Long
+)
+
+class LogsManager {
+ val logcatMessages = MutableLiveData>()
+ private val logcatMessagesLock = ReentrantLock()
+ private val logcatMessagesCondition = logcatMessagesLock.newCondition()
+ private val logcatMessagesThread = Thread {
+ // Clear the logcat
+ Runtime.getRuntime().exec("logcat -c").waitFor()
+ val process = Runtime.getRuntime().exec("logcat -v threadtime")
+ val reader = BufferedReader(InputStreamReader(process.inputStream))
+ var line: String?
+ while (true) {
+ line = reader.readLine()
+ if (line != null) {
+ val logcatMessage = parseLogcatMessage(line)
+ if (logcatMessage != null) {
+ // if the tag is included in the list of tags not to be displayed, skip it
+ val tagsToSkip =
+ listOf(
+ "Quality",
+ "AutofillManager",
+ "InputMethodManager",
+ "InsetsController",
+ "ProfileInstaller",
+ "CompatibilityChangeReporter",
+ "Activity",
+ "Choreographer",
+ "Looper",
+ "OpenGLRenderer",
+ "OplusSystemUINavigationGesture",
+ "System",
+ "BLASTBufferQueue",
+ )
+ if (logcatMessage.tag !in tagsToSkip) {
+ logcatMessagesLock.withLock {
+ logcatMessages.postValue(
+ logcatMessages.value?.plus(logcatMessage) ?: listOf(
+ logcatMessage
+ )
+ )
+ logcatMessagesCondition.signal()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ init {
+ logcatMessagesThread.start()
+ }
+
+ private fun parseLogcatMessage(line: String): LogcatMessage? {
+ val regex = Regex("([0-9-]+ [0-9:.]+) +([0-9]+) +([0-9]+) +([A-Z]) +([^:]+): (.*)")
+ val matchResult = regex.find(line)
+ if (matchResult != null) {
+ val groups = matchResult.groups
+ val time = GregorianCalendar().apply {
+ val date = groups[1]!!.value.split(" ")
+ val dateParts = date[0].split("-")
+ val timeParts = date[1].split(":")
+ set(
+ /* Current year */ get(GregorianCalendar.YEAR),
+ dateParts[0].toInt(),
+ dateParts[1].toInt(),
+ timeParts[0].toInt(),
+ timeParts[1].toInt(),
+ timeParts[2].split(".")[0].toInt()
+ )
+ }.timeInMillis
+ return LogcatMessage(
+ pid = groups[2]!!.value.trim(),
+ tid = groups[3]!!.value.trim(),
+ priority = groups[4]!!.value.trim(),
+ tag = groups[5]!!.value.trim(),
+ message = groups[6]!!.value.trim(),
+ time = time
+ )
+ }
+ return null
+ }
+}
diff --git a/app/src/main/java/com/cstef/meshlink/managers/ServerBleManager.kt b/app/src/main/java/com/cstef/meshlink/managers/ServerBleManager.kt
index fce48c0..711792e 100644
--- a/app/src/main/java/com/cstef/meshlink/managers/ServerBleManager.kt
+++ b/app/src/main/java/com/cstef/meshlink/managers/ServerBleManager.kt
@@ -191,11 +191,12 @@ class ServerBleManager(
callbackHandler.post {
device?.address?.let { dataExchangeManager.connect(it) }
}
- } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
- callbackHandler.post {
- dataExchangeManager.onUserDisconnected(device?.address!!)
- }
}
+// else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+// callbackHandler.post {
+// dataExchangeManager.onUserDisconnected(device?.address!!)
+// }
+// }
}
}
@@ -223,6 +224,7 @@ class ServerBleManager(
.addServiceUuid(ParcelUuid.fromString(BleUuid.SERVICE_UUID))
.setIncludeDeviceName(false)
.setIncludeTxPowerLevel(false)
+ //.addManufacturerData(0xDEAD, byteArrayOf(0x00))
.build()
Log.d(
"ServerBleManager",
diff --git a/app/src/main/java/com/cstef/meshlink/screens/BenchmarkScreen.kt b/app/src/main/java/com/cstef/meshlink/screens/BenchmarkScreen.kt
new file mode 100644
index 0000000..316f1a2
--- /dev/null
+++ b/app/src/main/java/com/cstef/meshlink/screens/BenchmarkScreen.kt
@@ -0,0 +1,64 @@
+package com.cstef.meshlink.screens
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.cstef.meshlink.BleService
+import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
+import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
+import com.patrykandpatrick.vico.compose.chart.Chart
+import com.patrykandpatrick.vico.compose.chart.line.lineChart
+import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
+import com.patrykandpatrick.vico.core.entry.FloatEntry
+
+@Composable
+fun BenchmarkScreen(binder: BleService.BleServiceBinder) {
+ val benchmarkResults = remember { mutableStateOf(emptyList()) }
+ val benchmarkRunning = remember {
+ mutableStateOf(false)
+ }
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Button(
+ onClick = {
+ benchmarkRunning.value = true
+ benchmarkResults.value = benchmark(binder)
+ benchmarkRunning.value = false
+ },
+ enabled = !benchmarkRunning.value,
+ content = {
+ Text("Run Benchmark")
+ }
+ )
+ if (benchmarkResults.value.isNotEmpty()) {
+ val producer = ChartEntryModelProducer(benchmarkResults.value.mapIndexed { index, value ->
+ FloatEntry(
+ index.toFloat(),
+ value.toFloat()
+ )
+ })
+ Chart(
+ chart = lineChart(),
+ chartModelProducer = producer,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(300.dp),
+ startAxis = startAxis(),
+ bottomAxis = bottomAxis(),
+ )
+ }
+ }
+}
+
+fun benchmark(binder: BleService.BleServiceBinder): List {
+ return emptyList()
+}
diff --git a/app/src/main/java/com/cstef/meshlink/screens/LogsScreen.kt b/app/src/main/java/com/cstef/meshlink/screens/LogsScreen.kt
new file mode 100644
index 0000000..2879e03
--- /dev/null
+++ b/app/src/main/java/com/cstef/meshlink/screens/LogsScreen.kt
@@ -0,0 +1,113 @@
+package com.cstef.meshlink.screens
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ClearAll
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.MutableLiveData
+import com.cstef.meshlink.managers.LogcatMessage
+
+@Composable
+fun LogsScreen(logcatLogs: MutableLiveData>, clearLogs: () -> Unit) {
+ val logs by logcatLogs.observeAsState(listOf())
+ LogsList(logs = logs, clearLogs = clearLogs)
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun LogsList(logs: List, clearLogs: () -> Unit) {
+ val colors = MaterialTheme.colorScheme
+ // go to the bottom of the list when new logs are added
+ val scrollState = rememberScrollState()
+ LaunchedEffect(logs.size) {
+ scrollState.animateScrollTo(logs.size * 100)
+ }
+ val (filter, setFilter) = remember { mutableStateOf("") }
+ Column(modifier = Modifier.fillMaxSize()) {
+ TopAppBar(
+ title = {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ OutlinedTextField(
+ value = filter,
+ onValueChange = setFilter,
+ label = { Text("Filter") },
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
+ .align(Alignment.CenterVertically),
+ singleLine = true,
+ textStyle = MaterialTheme.typography.bodyMedium,
+ )
+ IconButton(
+ onClick = {
+ clearLogs()
+ },
+ modifier = Modifier
+ .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
+ .align(Alignment.CenterVertically)
+ ) {
+ Icon(
+ imageVector = Icons.Default.ClearAll,
+ contentDescription = "Clear logs",
+ tint = colors.onSurface
+ )
+ }
+ }
+ },
+ modifier = Modifier.height(96.dp),
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = colors.background,
+ ),
+ )
+ LazyColumn(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ items(logs.filter {
+ it.message.contains(filter, true) || it.tag.contains(
+ filter, true
+ )
+ }) { log ->
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(Color.Gray)
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = log.tag, modifier = Modifier.padding(start = 8.dp), color = when (log.priority) {
+ "V" -> colors.onSurface
+ "D" -> colors.primary
+ "I" -> colors.secondary
+ "W" -> colors.tertiary
+ "E" -> colors.error
+ else -> colors.onSurface
+ }, style = MaterialTheme.typography.bodyMedium
+ )
+ Text(
+ text = log.message,
+ modifier = Modifier.padding(start = 8.dp),
+ color = colors.onBackground,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/cstef/meshlink/screens/SettingsScreen.kt b/app/src/main/java/com/cstef/meshlink/screens/SettingsScreen.kt
index abb8127..0287b62 100644
--- a/app/src/main/java/com/cstef/meshlink/screens/SettingsScreen.kt
+++ b/app/src/main/java/com/cstef/meshlink/screens/SettingsScreen.kt
@@ -4,6 +4,7 @@ import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.Feed
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -19,6 +20,8 @@ fun SettingsScreen(
stopAdvertising: () -> Unit,
goToAbout: () -> Unit,
goToStats: () -> Unit,
+ goToLogs: () -> Unit,
+ goToBenchmark: () -> Unit,
deleteAllData: () -> Unit,
changeDatabasePassword: (password: String) -> Boolean,
) {
@@ -30,6 +33,30 @@ fun SettingsScreen(
modifier = Modifier
.height(56.dp)
.padding(top = 16.dp),
+ actions = {
+ Row(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(end = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.End
+ ) {
+ IconButton(onClick = goToLogs) {
+ Icon(
+ imageVector = Icons.Filled.Feed,
+ contentDescription = "Go to logs screen",
+ tint = colors.onSurface
+ )
+ }
+// IconButton(onClick = goToBenchmark) {
+// Icon(
+// imageVector = Icons.Outlined.TrendingUp,
+// contentDescription = "Go to benchmark screen",
+// tint = colors.onSurface
+// )
+// }
+ }
+ }
)
Column {
Column(
diff --git a/app/src/main/java/com/cstef/meshlink/screens/UserInfoScreen.kt b/app/src/main/java/com/cstef/meshlink/screens/UserInfoScreen.kt
index 7f06daf..c6c1d38 100644
--- a/app/src/main/java/com/cstef/meshlink/screens/UserInfoScreen.kt
+++ b/app/src/main/java/com/cstef/meshlink/screens/UserInfoScreen.kt
@@ -1,5 +1,6 @@
package com.cstef.meshlink.screens
+import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@@ -18,6 +19,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.LiveData
import com.cstef.meshlink.db.entities.Device
import com.cstef.meshlink.ui.components.DeviceID
+import kotlin.math.pow
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -65,6 +67,19 @@ fun UserInfoScreen(
color = colors.onBackground
)
}
+ // Formula: Distance = 10 ^ ((Measured Power - RSSI)/(10 * N)) where N = 2 (in free space) and N = 3 (in walls), Measured Power = -50 (at 1 meter)
+ if (device?.connected == true && device.rssi != 0) {
+ Log.d("UserInfoScreen", "RSSI: ${device.rssi}, TX Power: ${device.txPower}")
+ val distance = 10.0.pow(((-59.0) - device.rssi.toDouble()) / (10.0 * 3.0))
+ Text(
+ text = "~%.2f meters away".format(distance),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier
+ .padding(bottom = 16.dp)
+ .align(Alignment.CenterHorizontally),
+ color = colors.onBackground
+ )
+ }
publicKey?.let {
Text(
text = "Public key",
diff --git a/app/src/main/java/com/cstef/meshlink/ui/components/AddedDevice.kt b/app/src/main/java/com/cstef/meshlink/ui/components/AddedDevice.kt
index 8e0b29d..d03d452 100644
--- a/app/src/main/java/com/cstef/meshlink/ui/components/AddedDevice.kt
+++ b/app/src/main/java/com/cstef/meshlink/ui/components/AddedDevice.kt
@@ -70,8 +70,8 @@ fun AddedDevice(
Icon(
imageVector =
if (!device.connected) Icons.Rounded.SignalWifiOff
- else if (device.rssi == 0) Icons.Rounded.SignalWifiBad
- else if (device.rssi >= -60) Icons.Rounded.NetworkWifi
+ else if (device.rssi == 0) Icons.Rounded.QuestionMark
+ else if (device.rssi >= -60) Icons.Rounded.SignalWifi4Bar
else if (device.rssi >= -70) Icons.Rounded.NetworkWifi3Bar
else if (device.rssi >= -80) Icons.Rounded.NetworkWifi2Bar
else if (device.rssi >= -90) Icons.Rounded.NetworkWifi1Bar
diff --git a/app/src/main/java/com/cstef/meshlink/ui/components/TextCard.kt b/app/src/main/java/com/cstef/meshlink/ui/components/TextCard.kt
index fa331f5..78a000b 100644
--- a/app/src/main/java/com/cstef/meshlink/ui/components/TextCard.kt
+++ b/app/src/main/java/com/cstef/meshlink/ui/components/TextCard.kt
@@ -32,6 +32,7 @@ fun TextCard(
} else {
MaterialTheme.colorScheme.secondary
},
+ contentColor = MaterialTheme.colorScheme.onSecondary
)
) {
Row {
@@ -41,7 +42,10 @@ fun TextCard(
.fillMaxWidth()
.align(alignment = Alignment.CenterVertically)
) {
- Text(text = content, style = MaterialTheme.typography.bodyLarge)
+ Text(
+ text = content,
+ style = MaterialTheme.typography.bodyLarge,
+ )
}
}
}
diff --git a/app/src/main/java/com/cstef/meshlink/ui/theme/Color.kt b/app/src/main/java/com/cstef/meshlink/ui/theme/Color.kt
index 28403f4..de6a4fd 100755
--- a/app/src/main/java/com/cstef/meshlink/ui/theme/Color.kt
+++ b/app/src/main/java/com/cstef/meshlink/ui/theme/Color.kt
@@ -1,67 +1,67 @@
package com.cstef.meshlink.ui.theme
import androidx.compose.ui.graphics.Color
-val md_theme_light_primary = Color(0xFF6750A4)
+val md_theme_light_primary = Color(0xFF3950D2)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
-val md_theme_light_primaryContainer = Color(0xFFEADDFF)
-val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
-val md_theme_light_secondary = Color(0xFF625B71)
+val md_theme_light_primaryContainer = Color(0xFFDEE0FF)
+val md_theme_light_onPrimaryContainer = Color(0xFF000F5D)
+val md_theme_light_secondary = Color(0xFF5B5D72)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
-val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
-val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
-val md_theme_light_tertiary = Color(0xFF7D5260)
+val md_theme_light_secondaryContainer = Color(0xFFE0E1F9)
+val md_theme_light_onSecondaryContainer = Color(0xFF181A2C)
+val md_theme_light_tertiary = Color(0xFF77536D)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
-val md_theme_light_tertiaryContainer = Color(0xFFFFD8E4)
-val md_theme_light_onTertiaryContainer = Color(0xFF31111D)
-val md_theme_light_error = Color(0xFFB3261E)
+val md_theme_light_tertiaryContainer = Color(0xFFFFD7F1)
+val md_theme_light_onTertiaryContainer = Color(0xFF2D1228)
+val md_theme_light_error = Color(0xFFBA1A1A)
+val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
-val md_theme_light_errorContainer = Color(0xFFF9DEDC)
-val md_theme_light_onErrorContainer = Color(0xFF410E0B)
-val md_theme_light_outline = Color(0xFF79747E)
-val md_theme_light_background = Color(0xFFFFFBFE)
-val md_theme_light_onBackground = Color(0xFF1C1B1F)
-val md_theme_light_surface = Color(0xFFFFFBFE)
-val md_theme_light_onSurface = Color(0xFF1C1B1F)
-val md_theme_light_surfaceVariant = Color(0xFFE7E0EC)
-val md_theme_light_onSurfaceVariant = Color(0xFF49454F)
-val md_theme_light_inverseSurface = Color(0xFF313033)
-val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4)
-val md_theme_light_inversePrimary = Color(0xFFD0BCFF)
+val md_theme_light_onErrorContainer = Color(0xFF410002)
+val md_theme_light_background = Color(0xFFFFFBFF)
+val md_theme_light_onBackground = Color(0xFF1B1B1F)
+val md_theme_light_surface = Color(0xFFFFFBFF)
+val md_theme_light_onSurface = Color(0xFF1B1B1F)
+val md_theme_light_surfaceVariant = Color(0xFFE3E1EC)
+val md_theme_light_onSurfaceVariant = Color(0xFF46464F)
+val md_theme_light_outline = Color(0xFF767680)
+val md_theme_light_inverseOnSurface = Color(0xFFF3F0F4)
+val md_theme_light_inverseSurface = Color(0xFF303034)
+val md_theme_light_inversePrimary = Color(0xFFBBC3FF)
val md_theme_light_shadow = Color(0xFF000000)
-val md_theme_light_surfaceTint = Color(0xFF6750A4)
-val md_theme_light_outlineVariant = Color(0xFFCAC4D0)
+val md_theme_light_surfaceTint = Color(0xFF3950D2)
+val md_theme_light_outlineVariant = Color(0xFFC7C5D0)
val md_theme_light_scrim = Color(0xFF000000)
-val md_theme_dark_primary = Color(0xFFD0BCFF)
-val md_theme_dark_onPrimary = Color(0xFF381E72)
-val md_theme_dark_primaryContainer = Color(0xFF4F378B)
-val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF)
-val md_theme_dark_secondary = Color(0xFFCCC2DC)
-val md_theme_dark_onSecondary = Color(0xFF332D41)
-val md_theme_dark_secondaryContainer = Color(0xFF4A4458)
-val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8)
-val md_theme_dark_tertiary = Color(0xFFEFB8C8)
-val md_theme_dark_onTertiary = Color(0xFF492532)
-val md_theme_dark_tertiaryContainer = Color(0xFF633B48)
-val md_theme_dark_onTertiaryContainer = Color(0xFFFFD8E4)
-val md_theme_dark_error = Color(0xFFF2B8B5)
-val md_theme_dark_onError = Color(0xFF601410)
-val md_theme_dark_errorContainer = Color(0xFF8C1D18)
-val md_theme_dark_onErrorContainer = Color(0xFFF9DEDC)
-val md_theme_dark_outline = Color(0xFF938F99)
-val md_theme_dark_background = Color(0xFF1C1B1F)
-val md_theme_dark_onBackground = Color(0xFFE6E1E5)
-val md_theme_dark_surface = Color(0xFF1C1B1F)
-val md_theme_dark_onSurface = Color(0xFFE6E1E5)
-val md_theme_dark_surfaceVariant = Color(0xFF49454F)
-val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4D0)
-val md_theme_dark_inverseSurface = Color(0xFFE6E1E5)
-val md_theme_dark_inverseOnSurface = Color(0xFF313033)
-val md_theme_dark_inversePrimary = Color(0xFF6750A4)
+val md_theme_dark_primary = Color(0xFFBBC3FF)
+val md_theme_dark_onPrimary = Color(0xFF001D92)
+val md_theme_dark_primaryContainer = Color(0xFF1934BA)
+val md_theme_dark_onPrimaryContainer = Color(0xFFDEE0FF)
+val md_theme_dark_secondary = Color(0xFFC3C5DD)
+val md_theme_dark_onSecondary = Color(0xFF2D2F42)
+val md_theme_dark_secondaryContainer = Color(0xFF434559)
+val md_theme_dark_onSecondaryContainer = Color(0xFFE0E1F9)
+val md_theme_dark_tertiary = Color(0xFFE6BAD7)
+val md_theme_dark_onTertiary = Color(0xFF44263D)
+val md_theme_dark_tertiaryContainer = Color(0xFF5D3C54)
+val md_theme_dark_onTertiaryContainer = Color(0xFFFFD7F1)
+val md_theme_dark_error = Color(0xFFFFB4AB)
+val md_theme_dark_errorContainer = Color(0xFF93000A)
+val md_theme_dark_onError = Color(0xFF690005)
+val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+val md_theme_dark_background = Color(0xFF1B1B1F)
+val md_theme_dark_onBackground = Color(0xFFE4E1E6)
+val md_theme_dark_surface = Color(0xFF1B1B1F)
+val md_theme_dark_onSurface = Color(0xFFE4E1E6)
+val md_theme_dark_surfaceVariant = Color(0xFF46464F)
+val md_theme_dark_onSurfaceVariant = Color(0xFFC7C5D0)
+val md_theme_dark_outline = Color(0xFF90909A)
+val md_theme_dark_inverseOnSurface = Color(0xFF1B1B1F)
+val md_theme_dark_inverseSurface = Color(0xFFE4E1E6)
+val md_theme_dark_inversePrimary = Color(0xFF3950D2)
val md_theme_dark_shadow = Color(0xFF000000)
-val md_theme_dark_surfaceTint = Color(0xFFD0BCFF)
-val md_theme_dark_outlineVariant = Color(0xFF49454F)
+val md_theme_dark_surfaceTint = Color(0xFFBBC3FF)
+val md_theme_dark_outlineVariant = Color(0xFF46464F)
val md_theme_dark_scrim = Color(0xFF000000)
-val seed = Color(0xFF6750A4)
+val seed = Color(0xFF6177F9)
diff --git a/app/src/main/java/com/cstef/meshlink/util/struct/Message.kt b/app/src/main/java/com/cstef/meshlink/util/struct/Message.kt
index 4eecd17..8b0a676 100644
--- a/app/src/main/java/com/cstef/meshlink/util/struct/Message.kt
+++ b/app/src/main/java/com/cstef/meshlink/util/struct/Message.kt
@@ -12,6 +12,7 @@ data class Message(
) : java.io.Serializable {
class Type {
companion object {
+ const val BENCHMARK = "benchmark"
const val TEXT = "text"
const val IMAGE = "image"
// const val FILE = "file"
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index 6b78462..0000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index 6b78462..0000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611d..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a695..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f50..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae3..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/presentation/src/deck.md b/presentation/src/deck.md
index 6a41306..7bc9ecb 100644
--- a/presentation/src/deck.md
+++ b/presentation/src/deck.md
@@ -333,10 +333,6 @@ $$
---
-# Amélioration du Developer Experience (DX)
-
----
-
# Évaluation
* 3 scénarios