From 632ef70a5a3b7d350a7d5d2f11b8dc77ab055bfe Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:58:45 +0200 Subject: [PATCH 01/41] build pipeline test --- .github/workflows/android-build.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/android-build.yml diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml new file mode 100644 index 00000000..daf86061 --- /dev/null +++ b/.github/workflows/android-build.yml @@ -0,0 +1,29 @@ +name: Android Build + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Build with Gradle + run: ./gradlew build From f5f3c04dda4955a377d052fa286d8feb25771473 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:05:02 +0200 Subject: [PATCH 02/41] use newer java --- .github/workflows/android-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index daf86061..b174f6f5 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -16,10 +16,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '17' distribution: 'temurin' - name: Setup Gradle From 85f23c23fda54070302ab46152db57285f39848b Mon Sep 17 00:00:00 2001 From: alpermelkeli <108495629+alpermelkeli@users.noreply.github.com> Date: Thu, 10 Jul 2025 18:56:55 +0300 Subject: [PATCH 03/41] implement channel password management at local and /pass command handling. --- .../com/bitchat/android/ui/ChannelManager.kt | 18 +++++-- .../bitchat/android/ui/CommandProcessor.kt | 50 ++++++++++++++++++- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt b/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt index f0d6b4c1..641c65ef 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt @@ -284,9 +284,21 @@ class ChannelManager( state.setShowPasswordPrompt(false) state.setPasswordPromptChannel(null) } - - fun setChannelPassword(channel: String, password: String): Boolean { - return verifyChannelPassword(channel, password) + + fun setChannelPassword(channel: String, password: String) { + + channelPasswords[channel] = password + + channelKeys[channel] = deriveChannelKey(password, channel) + + state.setPasswordProtectedChannels( + state.getPasswordProtectedChannelsValue().toMutableSet().apply { add(channel) } + ) + + dataManager.saveChannelData( + state.getJoinedChannelsValue(), + state.getPasswordProtectedChannelsValue() + ) } // MARK: - Emergency Clear diff --git a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt index 616cf3c4..286c0934 100644 --- a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt +++ b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt @@ -39,6 +39,7 @@ class CommandProcessor( "/m", "/msg" -> handleMessageCommand(parts, meshService) "/w" -> handleWhoCommand() "/clear" -> handleClearCommand() + "/pass" -> handlePassCommand(parts, myPeerID) "/block" -> handleBlockCommand(parts, meshService) "/unblock" -> handleUnblockCommand(parts, meshService) "/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage) @@ -54,7 +55,8 @@ class CommandProcessor( if (parts.size > 1) { val channelName = parts[1] val channel = if (channelName.startsWith("#")) channelName else "#$channelName" - val success = channelManager.joinChannel(channel, null, myPeerID) + val password = if (parts.size > 2) parts[2] else null + val success = channelManager.joinChannel(channel, password, myPeerID) if (success) { val systemMessage = BitchatMessage( sender = "system", @@ -165,6 +167,52 @@ class CommandProcessor( } } } + + private fun handlePassCommand(parts: List, peerID: String) { + val currentChannel = state.getCurrentChannelValue() + + if (currentChannel == null) { + val systemMessage = BitchatMessage( + sender = "system", + content = "you must be in a channel to set a password.", + timestamp = Date(), + isRelay = false + ) + messageManager.addMessage(systemMessage) + return + } + + if (parts.size == 2){ + if(!channelManager.isChannelCreator(channel = currentChannel, peerID = peerID)){ + val systemMessage = BitchatMessage( + sender = "system", + content = "you must be the channel creator to set a password.", + timestamp = Date(), + isRelay = false + ) + channelManager.addChannelMessage(currentChannel,systemMessage,null) + return + } + val newPassword = parts[1] + channelManager.setChannelPassword(currentChannel, newPassword) + val systemMessage = BitchatMessage( + sender = "system", + content = "password changed for channel $currentChannel", + timestamp = Date(), + isRelay = false + ) + channelManager.addChannelMessage(currentChannel,systemMessage,null) + } + else{ + val systemMessage = BitchatMessage( + sender = "system", + content = "usage: /pass ", + timestamp = Date(), + isRelay = false + ) + channelManager.addChannelMessage(currentChannel,systemMessage,null) + } + } private fun handleBlockCommand(parts: List, meshService: Any) { if (parts.size > 1) { From 1098970810c97e48cc4a1f272e0e407c9acc4c1a Mon Sep 17 00:00:00 2001 From: Gaurav Vashisth Date: Thu, 10 Jul 2025 23:20:14 +0530 Subject: [PATCH 04/41] fixes vertical padding in permission explanation --- .../bitchat/android/onboarding/PermissionExplanationScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt index 802e6ab2..f09d9783 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt @@ -30,10 +30,11 @@ fun PermissionExplanationScreen( Column( modifier = Modifier .fillMaxSize() - .padding(24.dp) + .padding(horizontal = 24.dp) .verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(16.dp) ) { + Spacer(modifier = Modifier.height(24.dp)) // Header Column( modifier = Modifier.fillMaxWidth(), @@ -164,6 +165,7 @@ fun PermissionExplanationScreen( ) } } + Spacer(modifier = Modifier.height(24.dp)) } } From 3941fe1f7e9e583949bb20a5945f47f47735c25d Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Thu, 10 Jul 2025 14:05:29 -0700 Subject: [PATCH 05/41] feat: add monochrome/themed icon for Android 12+ --- .../res/drawable/ic_launcher_monochrome.xml | 60 +++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 3 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 3 +- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_launcher_monochrome.xml diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 00000000..c531b643 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6b78462d..a79cb4c6 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,6 @@ - + + 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 index 6b78462d..a79cb4c6 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,6 @@ - + + From 6c51fe46f68fd82ebb9e0f5e008f9ccf79b20bbd Mon Sep 17 00:00:00 2001 From: Ranch Camal Date: Thu, 10 Jul 2025 23:36:57 -0400 Subject: [PATCH 06/41] Update README.md Updated the git clone URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dd52e086..0ddfb1ac 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ This project is released into the public domain. See the [LICENSE](LICENSE.md) f 1. **Clone the repository:** ```bash - git clone https://github.com/your-username/bitchat-android.git + git clone https://github.com/permissionlesstech/bitchat-android.git cd bitchat-android ``` From dd022fc22f8a12a047527af5fa2d9a9431f1fc69 Mon Sep 17 00:00:00 2001 From: Arjun Adhikari <33110520+theArjun@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:03:19 +0545 Subject: [PATCH 07/41] fix: update repo path --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dd52e086..0ddfb1ac 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ This project is released into the public domain. See the [LICENSE](LICENSE.md) f 1. **Clone the repository:** ```bash - git clone https://github.com/your-username/bitchat-android.git + git clone https://github.com/permissionlesstech/bitchat-android.git cd bitchat-android ``` From 622ba5b7871331e876b2a1f06fa66766e8d7a3aa Mon Sep 17 00:00:00 2001 From: prudhvir3ddy Date: Fri, 11 Jul 2025 16:58:18 +0530 Subject: [PATCH 08/41] release mode to be minified and shrinked --- app/build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8aad3797..ce8717bd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,7 +23,8 @@ android { buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true + isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" From 9bdfa31d5579b510da27207637603323914d00be Mon Sep 17 00:00:00 2001 From: prudhvir3ddy Date: Fri, 11 Jul 2025 17:15:54 +0530 Subject: [PATCH 09/41] Upgrade compileSDK, kotlin, AGP, compose, core libs --- app/build.gradle.kts | 4 +-- .../java/com/bitchat/android/MainActivity.kt | 4 +-- build.gradle.kts | 1 + gradle/libs.versions.toml | 32 +++++++++---------- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8aad3797..4220116d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.kotlin.compose) } android { @@ -40,9 +41,6 @@ android { buildFeatures { compose = true } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index a1f5390c..5ac59bb5 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -318,11 +318,11 @@ class MainActivity : ComponentActivity() { } } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) // Handle notification intents when app is already running if (onboardingState == OnboardingState.COMPLETE) { - intent?.let { handleNotificationIntent(it) } + handleNotificationIntent(intent) } } diff --git a/build.gradle.kts b/build.gradle.kts index 38d6e783..daf86da9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,5 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.compose) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 282370a4..eb1422bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,36 +1,35 @@ [versions] # Android and Kotlin -agp = "8.2.0" -kotlin = "1.9.20" -compileSdk = "34" +agp = "8.10.1" +kotlin = "2.2.0" +compileSdk = "35" minSdk = "26" # API 26 for proper BLE support targetSdk = "34" # AndroidX Core -core-ktx = "1.12.0" -lifecycle-runtime = "2.7.0" -activity-compose = "1.8.2" -appcompat = "1.6.1" +core-ktx = "1.16.0" +lifecycle-runtime = "2.9.1" +activity-compose = "1.10.1" +appcompat = "1.7.1" # Compose -compose-bom = "2023.10.01" -compose-compiler = "1.5.5" +compose-bom = "2025.06.01" # Navigation -navigation-compose = "2.7.6" +navigation-compose = "2.9.1" # Accompanist -accompanist-permissions = "0.32.0" +accompanist-permissions = "0.37.3" # Cryptography bouncycastle = "1.70" tink-android = "1.10.0" # JSON -gson = "2.10.1" +gson = "2.13.1" # Coroutines -kotlinx-coroutines = "1.7.3" +kotlinx-coroutines = "1.10.2" # Bluetooth nordic-ble = "2.6.1" @@ -39,12 +38,12 @@ nordic-ble = "2.6.1" lz4-java = "1.8.0" # Security -security-crypto = "1.1.0-alpha06" +security-crypto = "1.1.0-beta01" # Testing junit = "4.13.2" -androidx-test-ext = "1.1.5" -espresso = "3.5.1" +androidx-test-ext = "1.2.1" +espresso = "3.6.1" [libraries] # AndroidX Core @@ -104,6 +103,7 @@ android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-parcelize = { id = "kotlin-parcelize" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } [bundles] compose = [ From 5b2cbe951692070194ead0d517900b98587429d6 Mon Sep 17 00:00:00 2001 From: Shubert Munthali Date: Fri, 11 Jul 2025 14:50:59 +0200 Subject: [PATCH 10/41] Add unit tests package and migrate test_color.kt to unit testing --- app/src/test/kotlin/com/bitchat/ColorTest.kt | 78 ++++++++++++++++++++ test_color.kt | 51 ------------- 2 files changed, 78 insertions(+), 51 deletions(-) create mode 100644 app/src/test/kotlin/com/bitchat/ColorTest.kt delete mode 100644 test_color.kt diff --git a/app/src/test/kotlin/com/bitchat/ColorTest.kt b/app/src/test/kotlin/com/bitchat/ColorTest.kt new file mode 100644 index 00000000..f21a571c --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/ColorTest.kt @@ -0,0 +1,78 @@ +package com.bitchat + +import androidx.compose.ui.graphics.Color +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Generate a consistent color for a username based on their peer ID or nickname + * Returns colors that work well on both light and dark backgrounds + */ + +class ColorTest { + fun getUsernameColor(identifier: String): Color { + // Hash the identifier to get a consistent number + val hash = identifier.hashCode().toUInt() + + // Terminal-friendly colors that work on both black and white backgrounds + val colors = listOf( + Color(0xFF00FF00), // Bright Green + Color(0xFF00FFFF), // Cyan + Color(0xFFFFFF00), // Yellow + Color(0xFFFF00FF), // Magenta + Color(0xFF0080FF), // Bright Blue + Color(0xFFFF8000), // Orange + Color(0xFF80FF00), // Lime Green + Color(0xFF8000FF), // Purple + Color(0xFFFF0080), // Pink + Color(0xFF00FF80), // Spring Green + Color(0xFF80FFFF), // Light Cyan + Color(0xFFFF8080), // Light Red + Color(0xFF8080FF), // Light Blue + Color(0xFFFFFF80), // Light Yellow + Color(0xFFFF80FF), // Light Magenta + Color(0xFF80FF80), // Light Green + ) + + // Use modulo to get consistent color for same identifier + return colors[(hash % colors.size.toUInt()).toInt()] + } + + @Test + fun is_username_derived_color_consistent() { + + println("Testing username color function:") + + val testUsers = listOf("alice", "bob", "charlie", "diana", "eve") + + testUsers.forEach { user -> + val color = getUsernameColor(user) + println("User '$user' gets color: ${color.value.toString(16).uppercase()}") + } + + val `alice'sColor` = getUsernameColor(testUsers[0]) + val `bob'sColor` = getUsernameColor(testUsers[1]) + val `charlie'sColor` = getUsernameColor(testUsers[2]) + val `diana'sColor` = getUsernameColor(testUsers[3]) + val `eve'sColor` = getUsernameColor(testUsers[4]) + + // Test consistency - same user should always get same color + println("\nTesting consistency:") + repeat(3) { + val `alice's_color` = getUsernameColor(testUsers[0]) + val `bob's_color` = getUsernameColor(testUsers[1]) + val `charlie's_color` = getUsernameColor(testUsers[2]) + val `diana's_color` = getUsernameColor(testUsers[3]) + val `eve's_color` = getUsernameColor(testUsers[4]) + + + assertEquals(`alice'sColor`, `alice's_color`) + assertEquals(`bob'sColor`, `bob's_color`) + assertEquals(`charlie'sColor`, `charlie's_color`) + assertEquals(`diana'sColor`, `diana's_color`) + assertEquals(`eve'sColor`, `eve's_color`) + + println("Alice color (test ${it + 1}): ${`alice'sColor`.value.toString(16).uppercase()}") + } + } +} \ No newline at end of file diff --git a/test_color.kt b/test_color.kt deleted file mode 100644 index f13cc7d2..00000000 --- a/test_color.kt +++ /dev/null @@ -1,51 +0,0 @@ -import androidx.compose.ui.graphics.Color - -/** - * Generate a consistent color for a username based on their peer ID or nickname - * Returns colors that work well on both light and dark backgrounds - */ -fun getUsernameColor(identifier: String): Color { - // Hash the identifier to get a consistent number - val hash = identifier.hashCode().toUInt() - - // Terminal-friendly colors that work on both black and white backgrounds - val colors = listOf( - Color(0xFF00FF00), // Bright Green - Color(0xFF00FFFF), // Cyan - Color(0xFFFFFF00), // Yellow - Color(0xFFFF00FF), // Magenta - Color(0xFF0080FF), // Bright Blue - Color(0xFFFF8000), // Orange - Color(0xFF80FF00), // Lime Green - Color(0xFF8000FF), // Purple - Color(0xFFFF0080), // Pink - Color(0xFF00FF80), // Spring Green - Color(0xFF80FFFF), // Light Cyan - Color(0xFFFF8080), // Light Red - Color(0xFF8080FF), // Light Blue - Color(0xFFFFFF80), // Light Yellow - Color(0xFFFF80FF), // Light Magenta - Color(0xFF80FF80), // Light Green - ) - - // Use modulo to get consistent color for same identifier - return colors[(hash % colors.size.toUInt()).toInt()] -} - -fun main() { - println("Testing username color function:") - - val testUsers = listOf("alice", "bob", "charlie", "diana", "eve") - - testUsers.forEach { user -> - val color = getUsernameColor(user) - println("User '$user' gets color: ${color.value.toString(16).uppercase()}") - } - - // Test consistency - same user should always get same color - println("\nTesting consistency:") - repeat(3) { - val aliceColor = getUsernameColor("alice") - println("Alice color (test ${it + 1}): ${aliceColor.value.toString(16).uppercase()}") - } -} From fa3bb69d744c085e05066873da16ca451743aa40 Mon Sep 17 00:00:00 2001 From: maximal Date: Fri, 11 Jul 2025 18:02:44 +0300 Subject: [PATCH 11/41] =?UTF-8?q?Optimize=20PNG=20images=20losslessly:=201?= =?UTF-8?q?2.4=20=E2=86=92=2010.2=20KiB=20(=E2=88=9218%)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 628 -> 575 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 628 -> 575 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 765 -> 661 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 765 -> 661 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 848 -> 726 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 848 -> 726 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1423 -> 1115 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 1423 -> 1115 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 2709 -> 2162 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 2709 -> 2162 bytes 10 files changed, 0 insertions(+), 0 deletions(-) diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index fdc2d7a9e0d7007f833c9b38fa108ed7aec50963..096a00946c109c45ddd88b93da5ecdacf6fa2383 100644 GIT binary patch delta 346 zcmV-g0j2)*1iu83P=5z>Nliru=?4N3B0KLX_yqs}00Lr5M??VshmXv^0003aNklW4*RA(ENweLd+Orj4?*nD{t9=)|GKQWRubXqWlh~<^TWy delta 400 zcmV;B0dM}l1oQ-uP=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?mSUV<67G@2NLWDg8d<)BAjTp6Bg-UVjim2qDCOlTu14r5epMj|6ROH|#dNPz)7Lh-neTSL7NdG#_Y}G_RQz*zQK^;h4=yN`uuu0oSMQf#*!4DR#3(?kwpa+Rb;Sa!u_E=ntl)W1by0&?_ zLtso-j5Fxb7ArxEwpbIqXp2SRN>^-Z60&Y%wl84B4|T!*F4(#N%w-?~Q3FVn-K0}r ucl&GF0Q*1SgY)HRI5nT|in&bf`&bAegb?CSya5T8e+B17D~cJDodKc?$-CYF diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index fdc2d7a9e0d7007f833c9b38fa108ed7aec50963..096a00946c109c45ddd88b93da5ecdacf6fa2383 100644 GIT binary patch delta 346 zcmV-g0j2)*1iu83P=5z>Nliru=?4N3B0KLX_yqs}00Lr5M??VshmXv^0003aNklW4*RA(ENweLd+Orj4?*nD{t9=)|GKQWRubXqWlh~<^TWy delta 400 zcmV;B0dM}l1oQ-uP=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?mSUV<67G@2NLWDg8d<)BAjTp6Bg-UVjim2qDCOlTu14r5epMj|6ROH|#dNPz)7Lh-neTSL7NdG#_Y}G_RQz*zQK^;h4=yN`uuu0oSMQf#*!4DR#3(?kwpa+Rb;Sa!u_E=ntl)W1by0&?_ zLtso-j5Fxb7ArxEwpbIqXp2SRN>^-Z60&Y%wl84B4|T!*F4(#N%w-?~Q3FVn-K0}r ucl&GF0Q*1SgY)HRI5nT|in&bf`&bAegb?CSya5T8e+B17D~cJDodKc?$-CYF diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index fe8d9352db79512494670aef1ee40e5f1dc20b92..eb98d48bdff23efdbd4c1135ad622121c87600b4 100644 GIT binary patch delta 433 zcmV;i0Z#t?1(gMmP=5z>Nliru=?4N3B0KLX_yqs}00Lr5M??VshmXv^0004bNkl>G|^>o^QXW-{YK!h=_=Yh<}KPi2SN1-aO^fI}1IL z^0Nbc)asmb?vhH5z8o$(iyMVuGwLn;5QbqGW*=T|-HG@LufH5L4s8H1{{E^v;JDHw zLLm6waIKe)lK^K1VRrz~ZV;XU7%>Qs0VEB=(*SY*;YbRg#eaAnVAJ3=zmIbY#2xvkM^XA!>NOGHh=K)T!c9j>bS&crUjW1qoVJ&*^k8w^&i zL4ak0uuB0qQG@U-K(9f#A7IKL+ySs;5N-xoH3+u>tXaL|MT77Hz@-0hZ5ANrJzTd3 zaN>F8s_B}~0MPF#T--Zdz3!b#qg8qD^cUab!kn`R7bdnlo#${Ne{r}n(fGMP0zVFm bh=_=Yh=_=Yi2OItf7;U~c_P$(lUo9!YQ)N~ delta 538 zcmV+#0_FXc1^oq(P=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?_gm7L}oUBxG-?f%xu9qoaP=sehIX79C9E3 zAWU0u3E;tg=i_a!7%n12bN>0%DycL1F^gd>2nIfQ=! zH0BVl2k?D*r3;4ut2u>5sd18MB&z0(RX=-NF{XlAw^ zMNt&3uJ0U-k9J1m|1DNYJ|tpST-@_$u8a&huP$+`0V*>O=A64p;Yzu@GWUMst9MpJ cL_|bHL=QwnMC54L0&_|=`sj_l|C93pqTqe{s{jB1 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index fe8d9352db79512494670aef1ee40e5f1dc20b92..eb98d48bdff23efdbd4c1135ad622121c87600b4 100644 GIT binary patch delta 433 zcmV;i0Z#t?1(gMmP=5z>Nliru=?4N3B0KLX_yqs}00Lr5M??VshmXv^0004bNkl>G|^>o^QXW-{YK!h=_=Yh<}KPi2SN1-aO^fI}1IL z^0Nbc)asmb?vhH5z8o$(iyMVuGwLn;5QbqGW*=T|-HG@LufH5L4s8H1{{E^v;JDHw zLLm6waIKe)lK^K1VRrz~ZV;XU7%>Qs0VEB=(*SY*;YbRg#eaAnVAJ3=zmIbY#2xvkM^XA!>NOGHh=K)T!c9j>bS&crUjW1qoVJ&*^k8w^&i zL4ak0uuB0qQG@U-K(9f#A7IKL+ySs;5N-xoH3+u>tXaL|MT77Hz@-0hZ5ANrJzTd3 zaN>F8s_B}~0MPF#T--Zdz3!b#qg8qD^cUab!kn`R7bdnlo#${Ne{r}n(fGMP0zVFm bh=_=Yh=_=Yi2OItf7;U~c_P$(lUo9!YQ)N~ delta 538 zcmV+#0_FXc1^oq(P=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?_gm7L}oUBxG-?f%xu9qoaP=sehIX79C9E3 zAWU0u3E;tg=i_a!7%n12bN>0%DycL1F^gd>2nIfQ=! zH0BVl2k?D*r3;4ut2u>5sd18MB&z0(RX=-NF{XlAw^ zMNt&3uJ0U-k9J1m|1DNYJ|tpST-@_$u8a&huP$+`0V*>O=A64p;Yzu@GWUMst9MpJ cL_|bHL=QwnMC54L0&_|=`sj_l|C93pqTqe{s{jB1 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index f85ced2c5e541beabda19fe19341006ded6b82d5..fa1711674f51554e85eff38e6f8d4deaebaf17cb 100644 GIT binary patch delta 498 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 L0E36flkEheNa1+v delta 1151 zcmV-_1c3Y72#*VpP=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?-x;^vgq?2h*|S%^pR3=x zp0hqr+U}g5b7~Bekpmo&@C1JdMM*JtPG0(7x1Pyn|M;yht_PTI7~_I#vi=`TX6T&$ zJhWxStySk=d~hhDblyT+qAoPtCDAfX@zhNsnY_>R?cTOyz<=+tD)qUt#FA}CS4>q$ z5*A>LnX|g<# z*OlkSV`JFG3%-Dow{ElZ>oFXcnCu1Ae$Zy;!*8gQMda(ih%tY&A6Qv!e>i=AAWr}v z1(Z*lUfpR>S!uwldbetWX@Gj*=W;`mhf!^Uv(dwdT#tAG2t&()!H z1$^CcAahb3Nm)R$vZ`-?7&}2Ze%}Miu6=6v&o5rA4y7x=7-MF?(*1}!m9zk3YSwR` zq0SZA7~ca{ee8eclFCHw<@HVTE8IX6JCHZ=HM-B?7XOL94iX705kZ^T05@}<8yJ}{{@yG$xXAKP-l}A zkX>oVC*wK&W|9|RcG-htSKh1+Cn>;ew$mbYI86cj?bLs)4yP&LpgmD&Ue$Qk6Knwk zKjx+>A)F!*!#j1io$A%;GzI)>r#b3$ngUMPsaBm%Q-C`p62hkfF%;lVmaJ5#(-hz? zE%W}Fdl&_{BZPmc(`gE*u~Tdp6He{|n(P$9X+r)BXmlrvjegyOp8o<`+!(?!a_L00p`^pj+6V_S}jYp zK9qmSZ0AOGwm1uDxZTYqKZZ&JgwhJr^)EknKO-+tb~3q;ulFMCgZ?bJH&q_BR2Okpmo&@C6G% R=U)gQwvVMdnst-81ES3oJvsmY diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 6516c53c5bbd14b44292208b8d0b0252f24ab07a..7c81e83c76f9a47ea4c6ce3985357f92e210b2b9 100644 GIT binary patch delta 833 zcmV-H1HSx^3)={gP=5z>Nliru=?4N3B0KLX_yqs}00Lr5M??VshmXv^0009&NklaP!uU-Mc9J~%Cdz)DTReeP*9QdfY6AF z3d$COMbTpu5!j}p5-o8Nge@v86?%x`tCLZlb4?wGA*Js3{|qhVyYu)nIA`veJ4%zl z1R;M{3M?K;R(fD|?6}=$Zb_JYcj^4RiS2` zS9E{@?@ME32hjT6df@@Iu9_q~fPOMVcmVx8PIv%)%k2U39e<7VC#2>SUuzk1_XQ&y zzGvu4<+>Se`eO}~6vFz*99%zvE5VTi40eB5cHa@8b@G_ER%nLrhOt2W_W}&q)ME>j zNerM?_nL_}5fTHaO8XK>7a3rBhZ!jq89?1OBR6FR*l9)@Wd_JLBVS|&m}f@%Wd=yL zZv!5Y0kX_UyUbTWf%JcZ_sqxzkpWWdr-3|)0U~Sd*-@Xs01=n%d%$jip9T}`DGq<# z;1d@hv83HT|E@VoP5|$;S!o-OJ?wX!59(T>Tpl2}&r6l)05yq9aDdLL%n)zQjR9Vq z$cqp8*4!ANudy=6BRRlN9}dLJ4xn}0HsJxZzA?q^0rsa@scDPW>^k$f+gYG)rl6JJ zZ?PMkdxLc%_XZa{)uC?-xbl2JRE2*nuvO+Mv9o5RB~D}j-zzh-S7ZRkBWsQn89==@ zLzyxI95h2GWd_JFL(gOe@S3fGUcblys>w{vl^NiUnOZ9|z(q4vBs0KqGj&8}fWv00 zLS_KF5I8S0z-cpeNoIhnW~x$V0Q)78a+v|_g#*PR1EkuMLMufED7FETBnE$oervxb zB=XbX4*LSQA~C>1d%=Lt78oGVW}knvM_hoIvM$Hw&~_y&fG6Wrt8;s>KFZAj${oKT zv1IwG{C($YzYX0W?nxVVxSKAV9NU)Rhlh3P?Ee~|M2Qa2y;+G4@G_tz2l!MJtpo>X zC>-@Sh1?&Y^HEu_Z;87#!1omHuOFLgtB>Z-_Xa=wZw5#{G^kLMzyu-;fcyOb9+WX1 L0E36flkEheNa1+v delta 1151 zcmV-_1c3Y72#*VpP=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?-x;^vgq?2h*|S%^pR3=x zp0hqr+U}g5b7~Bekpmo&@C1JdMM*JtPG0(7x1Pyn|M;yht_PTI7~_I#vi=`TX6T&$ zJhWxStySk=d~hhDblyT+qAoPtCDAfX@zhNsnY_>R?cTOyz<=+tD)qUt#FA}CS4>q$ z5*A>LnX|g<# z*OlkSV`JFG3%-Dow{ElZ>oFXcnCu1Ae$Zy;!*8gQMda(ih%tY&A6Qv!e>i=AAWr}v z1(Z*lUfpR>S!uwldbetWX@Gj*=W;`mhf!^Uv(dwdT#tAG2t&()!H z1$^CcAahb3Nm)R$vZ`-?7&}2Ze%}Miu6=6v&o5rA4y7x=7-MF?(*1}!m9zk3YSwR` zq0SZA7~ca{ee8eclFCHw<@HVTE8IX6JCHZ=HM-B?7XOL94iX705kZ^T05@}<8yJ}{{@yG$xXAKP-l}A zkX>oVC*wK&W|9|RcG-htSKh1+Cn>;ew$mbYI86cj?bLs)4yP&LpgmD&Ue$Qk6Knwk zKjx+>A)F!*!#j1io$A%;GzI)>r#b3$ngUMPsaBm%Q-C`p62hkfF%;lVmaJ5#(-hz? zE%W}Fdl&_{BZPmc(`gE*u~Tdp6He{|n(P$9X+r)BXmlrvjegyOp8o<`+!(?!a_L00p`^pj+6V_S}jYp zK9qmSZ0AOGwm1uDxZTYqKZZ&JgwhJr^)EknKO-+tb~3q;ulFMCgZ?bJH&q_BR2Okpmo&@C6G% R=U)gQwvVMdnst-81ES3oJvsmY diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 5c73d4e63b78ddcbbbb7ab7a59dae287ceea3a1c..335769ff8b0ad99eb694df82ff65e208cb7d21df 100644 GIT binary patch delta 1779 zcmW-id0Z1&631UR2}uZ7L97knQ%bs!x4>Rqws7Z&hD5o;55v zh*DB~{Jba=6xh((b;fRy3;=-t=Igx|X!44?g#b_jefI7NNFSLJqXomM8{3u6!(nOG z(QfALCh zK}P;?+^(wMlh?42N=8y5Z{geC>AaW#(rBl&C5hH1;6#I9M&nl zKnp|x^6T|9sZ3nIDz~p|iCeoftt?!;8ooHtx2uuCyD~l)G$~fzJZ92UmiVFH;E1ZJ zX()hRb8~9oqn-rKwFqzI-UGARigF6)?KZ3%GJl}{Gm2cY8>{~LOH&oxW!~P=qm_2R z1o;T6V|!AcQ>~K05!LNrc>hL3waN@fH{nSEgG1Y)3Vr_1nfc%l<)C}=EyAd?atSka z-3A+g0t^b7h#XYc>F{r8wFsXHPfo}tUB+vf+XXRMGiwaZ?Ee-D{<*w%!N1D1;B=bl z(k6TP{L?#nx7Gq$!oG8G?`syD59ISrhQ6rL%Bn!ir6JXL=JYS!T| zX|ZK(Z98F6+C{K~bTH)ZD3e*g57@!B~nB1WKp3d=GqJ+vF!RXS3k0 zb;txQ0v@%&9H0kk4;{WY2!?Gf&%rJJAO_}U?JynZoFr6BhvwI@yzv;@Stqgzye9RU zs9ETPLwLm2fYkZU9&>~4&~q~aKnDD;BJz|8!UG-~bmtPF4>ZYp+0*(Yc)wBmO8HE- zlw9&HMlT0NOr#7vNxmClhdDu^7P^iy&qy8!Bz3L^<24q2SluU}3K~DvpVhq;cOv8G zzfr8i9m%7phf$`|yb<>%SW#?aqskx}#diM8M!Ns-Of7^Kjwd_bMpn4EZ&ffRvn z-PTQMI%421MtjZaDY>K@`hlj=9DQ`a5XlD~^F8Sqn3orz!R$G~2Kw2|n^gXU7WY{F z!y%nLSsP*QENK1its?ZLarv_Uc9{DNgt!!4V@uzWUj(8FBkr%aWkgq%`<_ebTl zHTD#Wb7`PjB>=6Xh4yflS-V)Q61zZ7hd?KXcX+1O2$7N$^HYv;VJgfm0J$siuEOz3 z5w1W$ap=O@-K(*mNxjrMaLO{Mjei(CX=>&e7lH#(&FRxXu z-uoqvFn1b+J543G?W6};V;>D9{QjBhB#r3C65M5o*p*IaZgPkm`vIz;@-I0r{6cmc z*-l_olzJRWqgtUppfc2mx8p+9vHF4TX`wLo1A$N=R#JXPgl{b{J^-l&$s<8 z(e~LFG8YC;L}VbVcdnF#M-rpAF>q9_kTW_7GGZ%ei5qW91K=$)2_A zami?~W~Y?gyB|u?C10hdQt`s*xIsryGz}MVsLxvW;`uGRO+RZB2gI<3_Tkp5HCvHD zD)|fcrXvU?sGmV=^)+?NsiTC*f7FW|QUd13Y{;$HlU zq^r4j@}x16quq-}=|wzF9UKpxQtfG$F6G{uL1Xv+7}Zm>ik29|?Ds%^JUykx zQ*SWqeIuM6OiJ`);0sd_O2QZEWhtqIjWIp`DxVpu7Tv- zOjfMpi0C#G(W2CA8Z7U&ELLp377p}`+a;suqpwguSj~TUI7H4;zi`GfI+hIS cbmG5p5z^Kjc#A^OoaY_8rCY;F=V;~s2Q1VNQUCw| delta 2333 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 DiM@wL diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 5c73d4e63b78ddcbbbb7ab7a59dae287ceea3a1c..335769ff8b0ad99eb694df82ff65e208cb7d21df 100644 GIT binary patch delta 1779 zcmW-id0Z1&631UR2}uZ7L97knQ%bs!x4>Rqws7Z&hD5o;55v zh*DB~{Jba=6xh((b;fRy3;=-t=Igx|X!44?g#b_jefI7NNFSLJqXomM8{3u6!(nOG z(QfALCh zK}P;?+^(wMlh?42N=8y5Z{geC>AaW#(rBl&C5hH1;6#I9M&nl zKnp|x^6T|9sZ3nIDz~p|iCeoftt?!;8ooHtx2uuCyD~l)G$~fzJZ92UmiVFH;E1ZJ zX()hRb8~9oqn-rKwFqzI-UGARigF6)?KZ3%GJl}{Gm2cY8>{~LOH&oxW!~P=qm_2R z1o;T6V|!AcQ>~K05!LNrc>hL3waN@fH{nSEgG1Y)3Vr_1nfc%l<)C}=EyAd?atSka z-3A+g0t^b7h#XYc>F{r8wFsXHPfo}tUB+vf+XXRMGiwaZ?Ee-D{<*w%!N1D1;B=bl z(k6TP{L?#nx7Gq$!oG8G?`syD59ISrhQ6rL%Bn!ir6JXL=JYS!T| zX|ZK(Z98F6+C{K~bTH)ZD3e*g57@!B~nB1WKp3d=GqJ+vF!RXS3k0 zb;txQ0v@%&9H0kk4;{WY2!?Gf&%rJJAO_}U?JynZoFr6BhvwI@yzv;@Stqgzye9RU zs9ETPLwLm2fYkZU9&>~4&~q~aKnDD;BJz|8!UG-~bmtPF4>ZYp+0*(Yc)wBmO8HE- zlw9&HMlT0NOr#7vNxmClhdDu^7P^iy&qy8!Bz3L^<24q2SluU}3K~DvpVhq;cOv8G zzfr8i9m%7phf$`|yb<>%SW#?aqskx}#diM8M!Ns-Of7^Kjwd_bMpn4EZ&ffRvn z-PTQMI%421MtjZaDY>K@`hlj=9DQ`a5XlD~^F8Sqn3orz!R$G~2Kw2|n^gXU7WY{F z!y%nLSsP*QENK1its?ZLarv_Uc9{DNgt!!4V@uzWUj(8FBkr%aWkgq%`<_ebTl zHTD#Wb7`PjB>=6Xh4yflS-V)Q61zZ7hd?KXcX+1O2$7N$^HYv;VJgfm0J$siuEOz3 z5w1W$ap=O@-K(*mNxjrMaLO{Mjei(CX=>&e7lH#(&FRxXu z-uoqvFn1b+J543G?W6};V;>D9{QjBhB#r3C65M5o*p*IaZgPkm`vIz;@-I0r{6cmc z*-l_olzJRWqgtUppfc2mx8p+9vHF4TX`wLo1A$N=R#JXPgl{b{J^-l&$s<8 z(e~LFG8YC;L}VbVcdnF#M-rpAF>q9_kTW_7GGZ%ei5qW91K=$)2_A zami?~W~Y?gyB|u?C10hdQt`s*xIsryGz}MVsLxvW;`uGRO+RZB2gI<3_Tkp5HCvHD zD)|fcrXvU?sGmV=^)+?NsiTC*f7FW|QUd13Y{;$HlU zq^r4j@}x16quq-}=|wzF9UKpxQtfG$F6G{uL1Xv+7}Zm>ik29|?Ds%^JUykx zQ*SWqeIuM6OiJ`);0sd_O2QZEWhtqIjWIp`DxVpu7Tv- zOjfMpi0C#G(W2CA8Z7U&ELLp377p}`+a;suqpwguSj~TUI7H4;zi`GfI+hIS cbmG5p5z^Kjc#A^OoaY_8rCY;F=V;~s2Q1VNQUCw| delta 2333 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 DiM@wL From 75c4b758b63063f7a357599dffb1aa64419a09fe Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:27:24 +0200 Subject: [PATCH 12/41] fix: /w shows usernames now --- .../main/java/com/bitchat/android/ui/CommandProcessor.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt index 616cf3c4..65063d9d 100644 --- a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt +++ b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt @@ -37,7 +37,7 @@ class CommandProcessor( when (cmd) { "/j", "/join" -> handleJoinCommand(parts, myPeerID) "/m", "/msg" -> handleMessageCommand(parts, meshService) - "/w" -> handleWhoCommand() + "/w" -> handleWhoCommand(meshService) "/clear" -> handleClearCommand() "/block" -> handleBlockCommand(parts, meshService) "/unblock" -> handleUnblockCommand(parts, meshService) @@ -127,11 +127,11 @@ class CommandProcessor( } } - private fun handleWhoCommand() { + private fun handleWhoCommand(meshService: Any) { val connectedPeers = state.getConnectedPeersValue() val peerList = connectedPeers.joinToString(", ") { peerID -> - // This would need mesh service access for nicknames - peerID // For now just use peer ID + // Convert peerID to nickname using the mesh service + getPeerNickname(peerID, meshService) } val systemMessage = BitchatMessage( From 5b5792c379ec3ac859a0d7e213b991e59fb35101 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:30:37 +0200 Subject: [PATCH 13/41] changelog --- CHANGELOG.md | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c586165d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,91 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Fixed +- `/w` command now displays user nicknames instead of peer IDs + +## [0.5.1] - 2025-07-10 + +### Added +- Bluetooth startup check with user prompt to enable Bluetooth if disabled + +### Fixed +- Improved Bluetooth initialization reliability on first app launch + +## [0.5] - 2025-07-10 + +### Added +- New user onboarding screen with permission explanations +- Educational content explaining why each permission is required +- Privacy assurance messaging (no tracking, no servers, local-only data) + +### Fixed +- Comprehensive permission validation - ensures all required permissions are granted +- Proper Bluetooth stack initialization on first app load +- Eliminated need for manual app restart after installation +- Enhanced permission request coordination and error handling + +### Changed +- Improved first-time user experience with guided setup flow + +## [0.4] - 2025-07-10 + +### Added +- Push notifications for direct messages +- Enhanced notification system with proper click handling and grouping + +### Improved +- Direct message (DM) view with better user interface +- Enhanced private messaging experience + +### Known Issues +- Favorite peer functionality currently broken + +## [0.3] - 2025-07-09 + +### Added +- Battery-aware scanning policies for improved power management +- Dynamic scan behavior based on device battery state + +### Fixed +- Android-to-Android Bluetooth Low Energy connections +- Peer discovery reliability between Android devices +- Connection stability improvements + +## [0.2] - 2025-07-09 + +### Added +- Initial Android implementation of bitchat protocol +- Bluetooth Low Energy mesh networking +- End-to-end encryption for private messages +- Channel-based messaging with password protection +- Store-and-forward message delivery +- IRC-style commands (/msg, /join, /clear, etc.) +- RSSI-based signal quality indicators + +### Fixed +- Various Bluetooth handling improvements +- User interface refinements +- Connection reliability enhancements + +## [0.1] - 2025-07-08 + +### Added +- Initial release of bitchat Android client +- Basic mesh networking functionality +- Core messaging features +- Protocol compatibility with iOS bitchat client + +[Unreleased]: https://github.com/permissionlesstech/bitchat-android/compare/0.5.1...HEAD +[0.5.1]: https://github.com/permissionlesstech/bitchat-android/compare/0.5...0.5.1 +[0.5]: https://github.com/permissionlesstech/bitchat-android/compare/0.4...0.5 +[0.4]: https://github.com/permissionlesstech/bitchat-android/compare/0.3...0.4 +[0.3]: https://github.com/permissionlesstech/bitchat-android/compare/0.2...0.3 +[0.2]: https://github.com/permissionlesstech/bitchat-android/compare/0.1...0.2 +[0.1]: https://github.com/permissionlesstech/bitchat-android/releases/tag/0.1 From e0065115d679e68989999fe4a3690f701f87e607 Mon Sep 17 00:00:00 2001 From: asmogo Date: Fri, 11 Jul 2025 22:31:38 +0200 Subject: [PATCH 14/41] Update references to new GitHub organization in project files. --- .github/DISCUSSION_TEMPLATE/question.yml | 4 ++-- .github/ISSUE_TEMPLATE/bug_report.yml | 6 +++--- .github/ISSUE_TEMPLATE/feature_request.yml | 4 ++-- .github/pull_request_template.md | 2 +- README.md | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/question.yml b/.github/DISCUSSION_TEMPLATE/question.yml index 0344e2f0..bb0c1a53 100644 --- a/.github/DISCUSSION_TEMPLATE/question.yml +++ b/.github/DISCUSSION_TEMPLATE/question.yml @@ -9,11 +9,11 @@ body: attributes: label: "Checklist" options: - - label: "I made sure that there are *no existing issues or discussions* - [open](https://github.com/callebtc/bitchat-android/issues) or [closed](https://github.com/callebtc/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." + - label: "I made sure that there are *no existing issues or discussions* - [open](https://github.com/permissionlesstech/bitchat-android/issues) or [closed](https://github.com/permissionlesstech/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." required: true - label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise." required: true - - label: "I have read and understood the [technical architecture](https://github.com/callebtc/bitchat-android/blob/main/README.md#technical-architecture)." + - label: "I have read and understood the [technical architecture](https://github.com/permissionlesstech/bitchat-android/blob/main/README.md#technical-architecture)." required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7e582683..cd10e3b0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,15 +14,15 @@ body: attributes: label: "Checklist" options: - - label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/callebtc/bitchat-android/releases/latest)." + - label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/permissionlesstech/bitchat-android/releases/latest)." required: true - - label: "I made sure that there are *no existing issues* - [open](https://github.com/callebtc/bitchat-android/issues) or [closed](https://github.com/callebtc/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." + - label: "I made sure that there are *no existing issues* - [open](https://github.com/permissionlesstech/bitchat-android/issues) or [closed](https://github.com/permissionlesstech/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." required: true - label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise." required: true - label: "This issue contains only one bug." required: true - - label: "I have read and understood the [contribution guidelines](https://github.com/callebtc/bitchat-android/blob/main/README.md#contributing)." + - label: "I have read and understood the [contribution guidelines](https://github.com/permissionlesstech/bitchat-android/blob/main/README.md#contributing)." required: true - type: input diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 095cdf1f..9a33e6ca 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -7,9 +7,9 @@ body: attributes: label: Checklist options: - - label: I have used the search function for [**OPEN**](https://github.com/callebtc/bitchat-android/issues) issues to see if someone else has already submitted the same feature request. + - label: I have used the search function for [**OPEN**](https://github.com/permissionlesstech/bitchat-android/issues) issues to see if someone else has already submitted the same feature request. required: true - - label: I have **also** used the search function for [**CLOSED**](https://github.com/callebtc/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) issues to see if the feature was already implemented and is just waiting to be released, or if the feature was rejected. + - label: I have **also** used the search function for [**CLOSED**](https://github.com/permissionlesstech/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) issues to see if the feature was already implemented and is just waiting to be released, or if the feature was rejected. required: true - label: I will describe the problem with as much detail as possible. required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index acc00ae4..5c31bf9c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,7 +6,7 @@ please make sure that you have done all of the following. You can tick the boxes below by placing an x inside the brackets like this: [x] --> -- [ ] I have read the contribution guidelines: +- [ ] I have read the contribution guidelines: - [ ] I have performed a self-review of my code - [ ] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see ) diff --git a/README.md b/README.md index 0ddfb1ac..b7a8a15e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This is the **Android port** of the original [bitchat iOS app](https://github.co ## Install bitchat -You can download the latest version of bitchat for Android from the [GitHub Releases page](https://github.com/callebtc/bitchat-android/releases). +You can download the latest version of bitchat for Android from the [GitHub Releases page](https://github.com/permissionlesstech/bitchat-android/releases). **Instructions:** From 83da8f3b1b1877762df339fa4e01be0c87de164b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:34:56 +0200 Subject: [PATCH 15/41] input --- .../java/com/bitchat/android/ui/ChatHeader.kt | 13 ++++++++++-- .../com/bitchat/android/ui/InputComponents.kt | 20 ++++++++----------- .../bitchat/android/ui/SidebarComponents.kt | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index e76dba4c..f7b85f0e 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -278,7 +278,15 @@ private fun ChannelHeader( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - IconButton(onClick = onBackClick) { + // Use same back button style as DM header (Button instead of IconButton) + Button( + onClick = onBackClick, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = colorScheme.primary + ), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + ) { Row( verticalAlignment = Alignment.CenterVertically ) { @@ -299,10 +307,11 @@ private fun ChannelHeader( Spacer(modifier = Modifier.weight(1f)) + // Centered title with orange color to match DM input field Text( text = "channel: $channel", style = MaterialTheme.typography.titleMedium, - color = Color(0xFF0080FF), // Blue + color = Color(0xFFFF8C00), // Orange to match input field modifier = Modifier.clickable { onSidebarClick() } ) diff --git a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt index cc25dc4f..e7706218 100644 --- a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt @@ -44,17 +44,13 @@ fun MessageInput( modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding verticalAlignment = Alignment.CenterVertically ) { - // Fixed: Remove arrow from private message input + // Remove arrow from both private and channel inputs to match DM style Text( - text = when { - selectedPrivatePeer != null -> "<@$nickname>" // Removed arrow for private - currentChannel != null -> "<@$nickname> →" // Keep arrow for channels - else -> "<@$nickname>" - }, + text = "<@$nickname>", // No arrow for both private and channel style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium), color = when { selectedPrivatePeer != null -> Color(0xFFFF8C00) // Orange for private - currentChannel != null -> Color(0xFFFF8C00) // Orange if encrypted channel + currentChannel != null -> Color(0xFFFF8C00) // Orange for channels too else -> colorScheme.primary }, fontFamily = FontFamily.Monospace @@ -78,7 +74,7 @@ fun MessageInput( Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing - // Fixed: Make send button orange in private mode to match nickname color + // Update send button to match input field colors IconButton( onClick = onSend, modifier = Modifier.size(32.dp) @@ -87,8 +83,8 @@ fun MessageInput( modifier = Modifier .size(30.dp) .background( - color = if (selectedPrivatePeer != null) { - // Orange for private messages to match nickname color + color = if (selectedPrivatePeer != null || currentChannel != null) { + // Orange for both private messages and channels to match nickname color Color(0xFFFF8C00).copy(alpha = 0.75f) } else if (colorScheme.background == Color.Black) { Color(0xFF00FF00).copy(alpha = 0.75f) // Bright green for dark theme @@ -103,8 +99,8 @@ fun MessageInput( imageVector = Icons.Filled.KeyboardArrowUp, contentDescription = "Send message", modifier = Modifier.size(20.dp), - tint = if (selectedPrivatePeer != null) { - // Black arrow on orange in private mode + tint = if (selectedPrivatePeer != null || currentChannel != null) { + // Black arrow on orange for both private and channel modes Color.Black } else if (colorScheme.background == Color.Black) { Color.Black // Black arrow on bright green in dark theme diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index e97b6446..b32821cc 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -193,7 +193,7 @@ fun ChannelsSection( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "#$channel", + text = channel, // Channel already contains the # prefix style = MaterialTheme.typography.bodyMedium, color = if (isSelected) colorScheme.primary else colorScheme.onSurface, fontWeight = if (isSelected) FontWeight.Medium else FontWeight.Normal, From 006ac7e78a88d2def8e813f582cefe9165c19642 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:37:51 +0200 Subject: [PATCH 16/41] buttons --- .../java/com/bitchat/android/ui/ChatHeader.kt | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index f7b85f0e..ffe0807b 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -201,19 +201,16 @@ private fun PrivateChatHeader( val colorScheme = MaterialTheme.colorScheme val peerNickname = peerNicknames[peerID] ?: peerID - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - // Fixed: Make back button wider to prevent text cropping + Box(modifier = Modifier.fillMaxWidth()) { + // Back button - positioned on the left Button( onClick = onBackClick, colors = ButtonDefaults.buttonColors( containerColor = Color.Transparent, contentColor = colorScheme.primary ), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), + modifier = Modifier.align(Alignment.CenterStart) ) { Row( verticalAlignment = Alignment.CenterVertically @@ -233,9 +230,11 @@ private fun PrivateChatHeader( } } - Spacer(modifier = Modifier.weight(1f)) - - Row(verticalAlignment = Alignment.CenterVertically) { + // Title - perfectly centered regardless of other elements + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.align(Alignment.Center) + ) { Icon( imageVector = Icons.Filled.Lock, contentDescription = "Private chat", @@ -250,10 +249,11 @@ private fun PrivateChatHeader( ) } - Spacer(modifier = Modifier.weight(1f)) - - // Favorite button with proper filled/outlined star - IconButton(onClick = onToggleFavorite) { + // Favorite button - positioned on the right + IconButton( + onClick = onToggleFavorite, + modifier = Modifier.align(Alignment.CenterEnd) + ) { Icon( imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star, contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites", @@ -273,19 +273,16 @@ private fun ChannelHeader( ) { val colorScheme = MaterialTheme.colorScheme - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - // Use same back button style as DM header (Button instead of IconButton) + Box(modifier = Modifier.fillMaxWidth()) { + // Back button - positioned on the left Button( onClick = onBackClick, colors = ButtonDefaults.buttonColors( containerColor = Color.Transparent, contentColor = colorScheme.primary ), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), + modifier = Modifier.align(Alignment.CenterStart) ) { Row( verticalAlignment = Alignment.CenterVertically @@ -305,19 +302,21 @@ private fun ChannelHeader( } } - Spacer(modifier = Modifier.weight(1f)) - - // Centered title with orange color to match DM input field + // Title - perfectly centered regardless of other elements Text( text = "channel: $channel", style = MaterialTheme.typography.titleMedium, color = Color(0xFFFF8C00), // Orange to match input field - modifier = Modifier.clickable { onSidebarClick() } + modifier = Modifier + .align(Alignment.Center) + .clickable { onSidebarClick() } ) - Spacer(modifier = Modifier.weight(1f)) - - TextButton(onClick = onLeaveChannel) { + // Leave button - positioned on the right + TextButton( + onClick = onLeaveChannel, + modifier = Modifier.align(Alignment.CenterEnd) + ) { Text( text = "leave", style = MaterialTheme.typography.bodySmall, From a9995ebfc1d48ae0f4db3df7ed4afcc35cdefa09 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:45:59 +0200 Subject: [PATCH 17/41] like --- .../main/java/com/bitchat/android/ui/ChatHeader.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index ffe0807b..ee674796 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -216,12 +216,11 @@ private fun PrivateChatHeader( verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = Icons.Filled.ArrowBack, + imageVector = Icons.Filled.ArrowBackIos, contentDescription = "Back", - modifier = Modifier.size(16.dp), + modifier = Modifier.size(14.dp), tint = colorScheme.primary ) - Spacer(modifier = Modifier.width(4.dp)) Text( text = "back", style = MaterialTheme.typography.bodyMedium, @@ -281,19 +280,17 @@ private fun ChannelHeader( containerColor = Color.Transparent, contentColor = colorScheme.primary ), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), modifier = Modifier.align(Alignment.CenterStart) ) { Row( verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = Icons.Filled.ArrowBack, + imageVector = Icons.Filled.ArrowBackIos, contentDescription = "Back", - modifier = Modifier.size(16.dp), + modifier = Modifier.size(14.dp), tint = colorScheme.primary - ) - Spacer(modifier = Modifier.width(4.dp)) + ) Text( text = "back", style = MaterialTheme.typography.bodyMedium, From 759ebaec93613c3bd661db8672589e1c0d9f84e9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:49:49 +0200 Subject: [PATCH 18/41] buttons --- .../java/com/bitchat/android/ui/ChatHeader.kt | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index ee674796..537d4afb 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -202,25 +202,28 @@ private fun PrivateChatHeader( val peerNickname = peerNicknames[peerID] ?: peerID Box(modifier = Modifier.fillMaxWidth()) { - // Back button - positioned on the left + // Back button - positioned all the way to the left with minimal margin Button( onClick = onBackClick, colors = ButtonDefaults.buttonColors( containerColor = Color.Transparent, contentColor = colorScheme.primary ), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), - modifier = Modifier.align(Alignment.CenterStart) + contentPadding = PaddingValues(horizontal = 4.dp, vertical = 4.dp), // Reduced horizontal padding + modifier = Modifier + .align(Alignment.CenterStart) + .offset(x = (-8).dp) // Move even further left to minimize margin ) { Row( verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = Icons.Filled.ArrowBackIos, + imageVector = Icons.Filled.ArrowBack, contentDescription = "Back", - modifier = Modifier.size(14.dp), + modifier = Modifier.size(16.dp), tint = colorScheme.primary ) + Spacer(modifier = Modifier.width(4.dp)) Text( text = "back", style = MaterialTheme.typography.bodyMedium, @@ -273,24 +276,28 @@ private fun ChannelHeader( val colorScheme = MaterialTheme.colorScheme Box(modifier = Modifier.fillMaxWidth()) { - // Back button - positioned on the left + // Back button - positioned all the way to the left with minimal margin Button( onClick = onBackClick, colors = ButtonDefaults.buttonColors( containerColor = Color.Transparent, contentColor = colorScheme.primary ), - modifier = Modifier.align(Alignment.CenterStart) + contentPadding = PaddingValues(horizontal = 4.dp, vertical = 4.dp), // Reduced horizontal padding + modifier = Modifier + .align(Alignment.CenterStart) + .offset(x = (-8).dp) // Move even further left to minimize margin ) { Row( verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = Icons.Filled.ArrowBackIos, + imageVector = Icons.Filled.ArrowBack, contentDescription = "Back", - modifier = Modifier.size(14.dp), + modifier = Modifier.size(16.dp), tint = colorScheme.primary - ) + ) + Spacer(modifier = Modifier.width(4.dp)) Text( text = "back", style = MaterialTheme.typography.bodyMedium, From 50995c670f34a0e5197fe463eb45da52334221d4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:55:14 +0200 Subject: [PATCH 19/41] orange --- app/src/main/java/com/bitchat/android/ui/ChatHeader.kt | 8 ++++---- app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt | 2 +- .../main/java/com/bitchat/android/ui/InputComponents.kt | 6 +++--- .../main/java/com/bitchat/android/ui/SidebarComponents.kt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index 537d4afb..d4372157 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -103,7 +103,7 @@ fun PeerCounter( imageVector = Icons.Filled.Email, contentDescription = "Unread private messages", modifier = Modifier.size(16.dp), - tint = Color(0xFFFF8C00) // Orange to match private message theme + tint = Color(0xFFFF9500) // Orange to match private message theme ) Spacer(modifier = Modifier.width(6.dp)) } @@ -241,13 +241,13 @@ private fun PrivateChatHeader( imageVector = Icons.Filled.Lock, contentDescription = "Private chat", modifier = Modifier.size(16.dp), - tint = Color(0xFFFF8C00) // Orange to match private message theme + tint = Color(0xFFFF9500) // Orange to match private message theme ) Spacer(modifier = Modifier.width(4.dp)) Text( text = peerNickname, style = MaterialTheme.typography.titleMedium, - color = Color(0xFFFF8C00) // Orange + color = Color(0xFFFF9500) // Orange ) } @@ -310,7 +310,7 @@ private fun ChannelHeader( Text( text = "channel: $channel", style = MaterialTheme.typography.titleMedium, - color = Color(0xFFFF8C00), // Orange to match input field + color = Color(0xFFFF9500), // Orange to match input field modifier = Modifier .align(Alignment.Center) .clickable { onSidebarClick() } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt b/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt index 6972106f..9d0a1476 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt @@ -139,7 +139,7 @@ private fun appendFormattedContent( } "mention" -> { builder.pushStyle(SpanStyle( - color = Color(0xFFFF8C00), // Orange + color = Color(0xFFFF9500), // Orange fontSize = 14.sp, fontWeight = FontWeight.SemiBold )) diff --git a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt index e7706218..ede412bf 100644 --- a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt @@ -49,8 +49,8 @@ fun MessageInput( text = "<@$nickname>", // No arrow for both private and channel style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium), color = when { - selectedPrivatePeer != null -> Color(0xFFFF8C00) // Orange for private - currentChannel != null -> Color(0xFFFF8C00) // Orange for channels too + selectedPrivatePeer != null -> Color(0xFFFF9500) // Orange for private + currentChannel != null -> Color(0xFFFF9500) // Orange for channels too else -> colorScheme.primary }, fontFamily = FontFamily.Monospace @@ -85,7 +85,7 @@ fun MessageInput( .background( color = if (selectedPrivatePeer != null || currentChannel != null) { // Orange for both private messages and channels to match nickname color - Color(0xFFFF8C00).copy(alpha = 0.75f) + Color(0xFFFF9500).copy(alpha = 0.75f) } else if (colorScheme.background == Color.Black) { Color(0xFF00FF00).copy(alpha = 0.75f) // Bright green for dark theme } else { diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index b32821cc..a7ee2c4f 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -320,7 +320,7 @@ private fun PeerItem( imageVector = Icons.Filled.Email, contentDescription = "Unread messages", modifier = Modifier.size(16.dp), - tint = Color(0xFFFF8C00) // Orange to match private message theme + tint = Color(0xFFFF9500) // Orange to match private message theme ) } else { // Signal strength indicators From 8e93500823df31f2e04210d868882dba82109562 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:58:30 +0200 Subject: [PATCH 20/41] nice --- app/src/main/java/com/bitchat/android/ui/InputComponents.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt index ede412bf..761b502b 100644 --- a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt @@ -53,7 +53,8 @@ fun MessageInput( currentChannel != null -> Color(0xFFFF9500) // Orange for channels too else -> colorScheme.primary }, - fontFamily = FontFamily.Monospace + fontFamily = FontFamily.Monospace, + fontSize = 14.sp ) Spacer(modifier = Modifier.width(8.dp)) @@ -96,7 +97,7 @@ fun MessageInput( contentAlignment = Alignment.Center ) { Icon( - imageVector = Icons.Filled.KeyboardArrowUp, + imageVector = Icons.Filled.KeyboardArrowRight, contentDescription = "Send message", modifier = Modifier.size(20.dp), tint = if (selectedPrivatePeer != null || currentChannel != null) { From a345d0a021a56a41164d544f3a2629e55d25bfc4 Mon Sep 17 00:00:00 2001 From: prudhvir3ddy Date: Sat, 12 Jul 2025 10:06:56 +0530 Subject: [PATCH 21/41] Refactor: Update permission handling to use PermissionType enum --- .../onboarding/PermissionExplanationScreen.kt | 32 +++++++++---------- .../android/onboarding/PermissionManager.kt | 17 +++++++--- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt index f09d9783..14470a1d 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt @@ -190,14 +190,14 @@ private fun PermissionCategoryCard( horizontalArrangement = Arrangement.spacedBy(12.dp) ) { Text( - text = getPermissionEmoji(category.name), + text = getPermissionEmoji(category.type), style = MaterialTheme.typography.titleLarge, - color = getPermissionIconColor(category.name), + color = getPermissionIconColor(category.type), modifier = Modifier.size(24.dp) ) Text( - text = category.name, + text = category.type.nameValue, style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.Bold, color = colorScheme.onSurface @@ -214,7 +214,7 @@ private fun PermissionCategoryCard( ) ) - if (category.name == "Precise Location") { + if (category.type == PermissionType.PRECISE_LOCATION) { // Extra emphasis for location permission Row( verticalAlignment = Alignment.CenterVertically, @@ -239,20 +239,20 @@ private fun PermissionCategoryCard( } } -private fun getPermissionEmoji(categoryName: String): String { - return when (categoryName) { - "Nearby Devices" -> "📱" - "Precise Location" -> "📍" - "Notifications" -> "🔔" - else -> "🔧" +private fun getPermissionEmoji(permissionType: PermissionType): String { + return when (permissionType) { + PermissionType.NEARBY_DEVICES -> "📱" + PermissionType.PRECISE_LOCATION -> "📍" + PermissionType.NOTIFICATIONS -> "🔔" + PermissionType.OTHER -> "🔧" } } -private fun getPermissionIconColor(categoryName: String): Color { - return when (categoryName) { - "Nearby Devices" -> Color(0xFF2196F3) // Blue - "Precise Location" -> Color(0xFFFF9800) // Orange - "Notifications" -> Color(0xFF4CAF50) // Green - else -> Color(0xFF9C27B0) // Purple +private fun getPermissionIconColor(permissionType: PermissionType): Color { + return when (permissionType) { + PermissionType.NEARBY_DEVICES -> Color(0xFF2196F3) // Blue + PermissionType.PRECISE_LOCATION -> Color(0xFFFF9800) // Orange + PermissionType.NOTIFICATIONS -> Color(0xFF4CAF50) // Green + PermissionType.OTHER -> Color(0xFF9C27B0) // Purple } } diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt index 7060fb1c..6ef3fc2b 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt @@ -115,7 +115,7 @@ class PermissionManager(private val context: Context) { categories.add( PermissionCategory( - name = "Nearby Devices", + type = PermissionType.NEARBY_DEVICES, description = "Required to discover and connect to other bitchat users via Bluetooth", permissions = bluetoothPermissions, isGranted = bluetoothPermissions.all { isPermissionGranted(it) }, @@ -131,7 +131,7 @@ class PermissionManager(private val context: Context) { categories.add( PermissionCategory( - name = "Precise Location", + type = PermissionType.PRECISE_LOCATION, description = "Required by Android for Bluetooth scanning.", permissions = locationPermissions, isGranted = locationPermissions.all { isPermissionGranted(it) }, @@ -143,7 +143,7 @@ class PermissionManager(private val context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { categories.add( PermissionCategory( - name = "Notifications", + type = PermissionType.NOTIFICATIONS, description = "Show notifications when you receive private messages while the app is in background", permissions = listOf(Manifest.permission.POST_NOTIFICATIONS), isGranted = isPermissionGranted(Manifest.permission.POST_NOTIFICATIONS), @@ -167,7 +167,7 @@ class PermissionManager(private val context: Context) { appendLine() getCategorizedPermissions().forEach { category -> - appendLine("${category.name}: ${if (category.isGranted) "✅ GRANTED" else "❌ MISSING"}") + appendLine("${category.type.nameValue}: ${if (category.isGranted) "✅ GRANTED" else "❌ MISSING"}") category.permissions.forEach { permission -> val granted = isPermissionGranted(permission) appendLine(" - ${permission.substringAfterLast(".")}: ${if (granted) "✅" else "❌"}") @@ -197,9 +197,16 @@ class PermissionManager(private val context: Context) { * Data class representing a category of related permissions */ data class PermissionCategory( - val name: String, + val type: PermissionType, val description: String, val permissions: List, val isGranted: Boolean, val systemDescription: String ) + +enum class PermissionType(val nameValue: String) { + NEARBY_DEVICES("Nearby Devices"), + PRECISE_LOCATION("Precise Location"), + NOTIFICATIONS("Notifications"), + OTHER("Other") +} From e58cf4fd0d3fbb9f48b756724749302e53a2e0a7 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:53:01 +0200 Subject: [PATCH 22/41] fix null pointer --- .../mesh/BluetoothConnectionManager.kt | 114 +++++++++++++----- .../android/mesh/BluetoothMeshService.kt | 26 +++- .../bitchat/android/mesh/SecurityManager.kt | 4 +- .../com/bitchat/android/ui/ChatViewModel.kt | 8 +- .../bitchat/android/ui/MeshDelegateHandler.kt | 4 + 5 files changed, 118 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index faaa3a45..83673d1f 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -128,11 +128,13 @@ class BluetoothConnectionManager( try { isActive = true - setupGattServer() // Start power manager and services connectionScope.launch { powerManager.start() + + // Setup GATT server after power manager is ready + setupGattServer() delay(500) // Ensure GATT server is ready startAdvertising() @@ -146,13 +148,14 @@ class BluetoothConnectionManager( startPeriodicCleanup() - Log.i(TAG, "Power-optimized Bluetooth services started successfully (CLIENT ONLY)") + Log.i(TAG, "Power-optimized Bluetooth services started successfully") } return true } catch (e: Exception) { Log.e(TAG, "Failed to start Bluetooth services: ${e.message}") + isActive = false return false } } @@ -355,6 +358,12 @@ class BluetoothConnectionManager( val serverCallback = object : BluetoothGattServerCallback() { override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring connection state change after shutdown") + return + } + when (newState) { BluetoothProfile.STATE_CONNECTED -> { Log.d(TAG, "Server: Device connected ${device.address}") @@ -371,6 +380,20 @@ class BluetoothConnectionManager( } } + override fun onServiceAdded(status: Int, service: BluetoothGattService) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring service added callback after shutdown") + return + } + + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.d(TAG, "Server: Service added successfully: ${service.uuid}") + } else { + Log.e(TAG, "Server: Failed to add service: ${service.uuid}, status: $status") + } + } + override fun onCharacteristicWriteRequest( device: BluetoothDevice, requestId: Int, @@ -380,6 +403,12 @@ class BluetoothConnectionManager( offset: Int, value: ByteArray ) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring characteristic write after shutdown") + return + } + if (characteristic.uuid == CHARACTERISTIC_UUID) { val packet = BitchatPacket.fromBinaryData(value) if (packet != null) { @@ -402,13 +431,21 @@ class BluetoothConnectionManager( offset: Int, value: ByteArray ) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring descriptor write after shutdown") + return + } + if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) { Log.d(TAG, "Device ${device.address} subscribed to notifications") subscribedDevices.add(device) connectionScope.launch { delay(100) - delegate?.onDeviceConnected(device) + if (isActive) { // Check if still active + delegate?.onDeviceConnected(device) + } } } @@ -418,34 +455,51 @@ class BluetoothConnectionManager( } } - // Clean up existing server - gattServer?.close() + // Proper cleanup sequencing to prevent race conditions + gattServer?.let { server -> + Log.d(TAG, "Cleaning up existing GATT server") + connectionScope.launch { + // Give time for pending callbacks to complete + delay(100) + server.close() + } + } - gattServer = bluetoothManager.openGattServer(context, serverCallback) - - // Create characteristic with notification support - characteristic = BluetoothGattCharacteristic( - CHARACTERISTIC_UUID, - BluetoothGattCharacteristic.PROPERTY_READ or - BluetoothGattCharacteristic.PROPERTY_WRITE or - BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or - BluetoothGattCharacteristic.PROPERTY_NOTIFY, - BluetoothGattCharacteristic.PERMISSION_READ or - BluetoothGattCharacteristic.PERMISSION_WRITE - ) - - val descriptor = BluetoothGattDescriptor( - UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), - BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE - ) - characteristic?.addDescriptor(descriptor) - - val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) - service.addCharacteristic(characteristic) - - gattServer?.addService(service) - - Log.i(TAG, "GATT server setup complete") + // Create new server after cleanup delay + connectionScope.launch { + delay(200) // Allow previous server to fully close + + if (!isActive) { + Log.d(TAG, "Service inactive, skipping GATT server creation") + return@launch + } + + gattServer = bluetoothManager.openGattServer(context, serverCallback) + + // Create characteristic with notification support + characteristic = BluetoothGattCharacteristic( + CHARACTERISTIC_UUID, + BluetoothGattCharacteristic.PROPERTY_READ or + BluetoothGattCharacteristic.PROPERTY_WRITE or + BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or + BluetoothGattCharacteristic.PROPERTY_NOTIFY, + BluetoothGattCharacteristic.PERMISSION_READ or + BluetoothGattCharacteristic.PERMISSION_WRITE + ) + + val descriptor = BluetoothGattDescriptor( + UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), + BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE + ) + characteristic?.addDescriptor(descriptor) + + val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) + service.addCharacteristic(characteristic) + + gattServer?.addService(service) + + Log.i(TAG, "GATT server setup complete") + } } @Suppress("DEPRECATION") diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 0a66bcf8..e92a6dfc 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -47,6 +47,9 @@ class BluetoothMeshService(private val context: Context) { internal val connectionManager = BluetoothConnectionManager(context, myPeerID) // Made internal for access private val packetProcessor = PacketProcessor(myPeerID) + // Service state management + private var isActive = false + // Delegate for message callbacks (maintains same interface) var delegate: BluetoothMeshDelegate? = null @@ -98,7 +101,10 @@ class BluetoothMeshService(private val context: Context) { // SecurityManager delegate for key exchange notifications securityManager.delegate = object : SecurityManagerDelegate { - override fun onKeyExchangeCompleted(peerID: String) { + override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) { + // Notify delegate about key exchange completion so it can register peer fingerprint + delegate?.registerPeerPublicKey(peerID, peerPublicKeyData) + // Send announcement and cached messages after key exchange serviceScope.launch { delay(100) @@ -276,15 +282,24 @@ class BluetoothMeshService(private val context: Context) { * Start the mesh service */ fun startServices() { + // Prevent double starts + if (isActive) { + Log.w(TAG, "Mesh service already active, ignoring duplicate start request") + return + } + Log.i(TAG, "Starting Bluetooth mesh service with peer ID: $myPeerID") if (connectionManager.startServices()) { + isActive = true Log.i(TAG, "Bluetooth services started successfully") // Send initial announcements after services are ready serviceScope.launch { delay(1000) - sendBroadcastAnnounce() + if (isActive) { // Check if still active + sendBroadcastAnnounce() + } } } else { Log.e(TAG, "Failed to start Bluetooth services") @@ -295,7 +310,13 @@ class BluetoothMeshService(private val context: Context) { * Stop all mesh services */ fun stopServices() { + if (!isActive) { + Log.w(TAG, "Mesh service not active, ignoring stop request") + return + } + Log.i(TAG, "Stopping Bluetooth mesh service") + isActive = false // Send leave announcement sendLeaveAnnouncement() @@ -547,4 +568,5 @@ interface BluetoothMeshDelegate { fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? fun getNickname(): String? fun isFavorite(peerID: String): Boolean + fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) } diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index 3b655d08..65b74e42 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -113,7 +113,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private Log.d(TAG, "Successfully processed key exchange from $peerID") // Notify delegate - delegate?.onKeyExchangeCompleted(peerID) + delegate?.onKeyExchangeCompleted(peerID, packet.payload) return true @@ -315,5 +315,5 @@ class SecurityManager(private val encryptionService: EncryptionService, private * Delegate interface for security manager callbacks */ interface SecurityManagerDelegate { - fun onKeyExchangeCompleted(peerID: String) + fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 7d9a5055..4240c1cf 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -253,10 +253,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B privateChatManager.toggleFavorite(peerID) } - fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) { - privateChatManager.registerPeerPublicKey(peerID, publicKeyData) - } - // MARK: - Debug and Troubleshooting fun getDebugStatus(): String { @@ -341,6 +337,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B return meshDelegateHandler.isFavorite(peerID) } + override fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) { + privateChatManager.registerPeerPublicKey(peerID, publicKeyData) + } + // MARK: - Emergency Clear fun panicClearAllData() { diff --git a/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt b/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt index 873d9c56..d8e8503a 100644 --- a/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt +++ b/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt @@ -154,4 +154,8 @@ class MeshDelegateHandler( override fun isFavorite(peerID: String): Boolean { return privateChatManager.isFavorite(peerID) } + + override fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) { + privateChatManager.registerPeerPublicKey(peerID, publicKeyData) + } } From d1085fde0b9afcaf6f84c5dd10cbc35b9cd5ddef Mon Sep 17 00:00:00 2001 From: GUVWAF Date: Fri, 11 Jul 2025 12:44:25 +0200 Subject: [PATCH 23/41] Cheap routing optimizations - Don't relay back to sender or relayer - Only send to connected device if recipientID matches one - Don't relay our own ACK/read receipt --- .../mesh/BluetoothConnectionManager.kt | 108 ++++++++++++++---- .../android/mesh/BluetoothMeshService.kt | 61 +++++----- .../bitchat/android/mesh/MessageHandler.kt | 51 ++++++--- .../bitchat/android/mesh/PacketProcessor.kt | 87 +++++++------- .../bitchat/android/mesh/SecurityManager.kt | 10 +- .../com/bitchat/android/model/RoutedPacket.kt | 13 +++ 6 files changed, 216 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/model/RoutedPacket.kt diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index 83673d1f..e729496a 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -9,6 +9,8 @@ import android.os.ParcelUuid import android.util.Log import androidx.core.app.ActivityCompat import com.bitchat.android.protocol.BitchatPacket +import com.bitchat.android.protocol.SpecialRecipients +import com.bitchat.android.model.RoutedPacket import kotlinx.coroutines.* import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -53,6 +55,7 @@ class BluetoothConnectionManager( // Simplified connection tracking - reduced memory footprint private val connectedDevices = ConcurrentHashMap() private val subscribedDevices = CopyOnWriteArrayList() + public val addressPeerMap = ConcurrentHashMap() // Connection attempt tracking with automatic cleanup private val pendingConnections = ConcurrentHashMap() @@ -199,47 +202,102 @@ class BluetoothConnectionManager( powerManager.setAppBackgroundState(inBackground) } + // Function to send data to a single device (server side) + private fun notifyDevice(device: BluetoothDevice, data: ByteArray) { + try { + characteristic?.let { char -> + char.value = data + gattServer?.notifyCharacteristicChanged(device, char, false) + } + } catch (e: Exception) { + Log.w(TAG, "Error sending to server connection ${device.address}: ${e.message}") + connectionScope.launch { + delay(CLEANUP_DELAY) + subscribedDevices.remove(device) + addressPeerMap.remove(device.getAddress()) + } + } + } + + // Function to send data to a single device (client side) + private fun writeToDeviceConn(deviceConn: DeviceConnection, data: ByteArray) { + try { + deviceConn.characteristic?.let { char -> + char.value = data + deviceConn.gatt?.writeCharacteristic(char) + } + } catch (e: Exception) { + Log.w(TAG, "Error sending to client connection ${deviceConn.device.address}: ${e.message}") + connectionScope.launch { + delay(CLEANUP_DELAY) + cleanupDeviceConnection(deviceConn.device.address) + } + } + } + /** * Broadcast packet to connected devices with connection limit enforcement */ - fun broadcastPacket(packet: BitchatPacket) { + fun broadcastPacket(routed: RoutedPacket) { + val packet = routed.packet + if (!isActive) return val data = packet.toBinaryData() ?: return + if (packet.recipientID != SpecialRecipients.BROADCAST) { + val recipientID = packet.recipientID?.let { + String(it).replace("\u0000", "").trim() + } ?: "" + + // Try to find the recipient in server connections (subscribedDevices) + val targetDevice = subscribedDevices.firstOrNull { addressPeerMap[it.address] == recipientID } + // If found, send directly + if (targetDevice != null) { + Log.d(TAG, "Send packet type ${packet.type} directly to target device for recipient $recipientID: ${targetDevice.address}") + notifyDevice(targetDevice, data) + return // Sent, no need to continue + } + + // Try to find the recipient in client connections (connectedDevices) + val targetDeviceConn = connectedDevices.values.firstOrNull { addressPeerMap[it.device.address] == recipientID } + // If found, send directly + if (targetDeviceConn != null) { + Log.d(TAG, "Send packet type ${packet.type} directly to target client connection for recipient $recipientID: ${targetDeviceConn.device.address}") + writeToDeviceConn(targetDeviceConn, data) + return // Sent, no need to continue + } + } + + // Else, continue with broadcasting to all devices Log.d(TAG, "Broadcasting packet type ${packet.type} to ${subscribedDevices.size} server + ${connectedDevices.size} client connections") - + + val senderID = String(packet.senderID).replace("\u0000", "") // Send to server connections (devices connected to our GATT server) subscribedDevices.forEach { device -> - try { - characteristic?.let { char -> - char.value = data - gattServer?.notifyCharacteristicChanged(device, char, false) - } - } catch (e: Exception) { - Log.w(TAG, "Error sending to server connection ${device.address}: ${e.message}") - // Clean up failed connection - connectionScope.launch { - delay(CLEANUP_DELAY) - subscribedDevices.remove(device) - } + if (device.address == routed.relayAddress) { + Log.d(TAG, "Skipping broadcast back to relayer: ${device.address}") + return@forEach } + if (addressPeerMap[device.address] == senderID) { + Log.d(TAG, "Skipping broadcast back to sender: ${device.address}") + return@forEach + } + notifyDevice(device, data) } // Send to client connections connectedDevices.values.forEach { deviceConn -> if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) { - try { - deviceConn.characteristic.value = data - deviceConn.gatt.writeCharacteristic(deviceConn.characteristic) - } catch (e: Exception) { - Log.w(TAG, "Error sending to client connection ${deviceConn.device.address}: ${e.message}") - // Clean up failed connection - connectionScope.launch { - delay(CLEANUP_DELAY) - cleanupDeviceConnection(deviceConn.device.address) - } + if (deviceConn.device.address == routed.relayAddress) { + Log.d(TAG, "Skipping broadcast back to relayer: ${deviceConn.device.address}") + return@forEach } + if (addressPeerMap[deviceConn.device.address] == senderID) { + Log.d(TAG, "Skipping broadcast back to sender: ${deviceConn.device.address}") + return@forEach + } + writeToDeviceConn(deviceConn, data) } } } @@ -907,6 +965,7 @@ class BluetoothConnectionManager( private fun cleanupDeviceConnection(deviceAddress: String) { connectedDevices.remove(deviceAddress)?.let { deviceConn -> subscribedDevices.removeAll { it.address == deviceAddress } + addressPeerMap.remove(deviceAddress) } // CRITICAL FIX: Always remove from pending connections when cleaning up // This prevents failed connections from blocking future attempts @@ -935,6 +994,7 @@ class BluetoothConnectionManager( private fun clearAllConnections() { connectedDevices.clear() subscribedDevices.clear() + addressPeerMap.clear() pendingConnections.clear() } } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index e92a6dfc..5d4634f0 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -5,6 +5,7 @@ import android.util.Log import com.bitchat.android.crypto.EncryptionService import com.bitchat.android.crypto.MessagePadding import com.bitchat.android.model.BitchatMessage +import com.bitchat.android.model.RoutedPacket import com.bitchat.android.model.DeliveryAck import com.bitchat.android.model.ReadReceipt import com.bitchat.android.protocol.BitchatPacket @@ -101,10 +102,14 @@ class BluetoothMeshService(private val context: Context) { // SecurityManager delegate for key exchange notifications securityManager.delegate = object : SecurityManagerDelegate { - override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) { + override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?) { // Notify delegate about key exchange completion so it can register peer fingerprint delegate?.registerPeerPublicKey(peerID, peerPublicKeyData) + receivedAddress?.let { address -> + connectionManager.addressPeerMap[address] = peerID + } + // Send announcement and cached messages after key exchange serviceScope.launch { delay(100) @@ -127,7 +132,7 @@ class BluetoothMeshService(private val context: Context) { } override fun sendPacket(packet: BitchatPacket) { - connectionManager.broadcastPacket(packet) + connectionManager.broadcastPacket(RoutedPacket(packet)) } } @@ -160,11 +165,11 @@ class BluetoothMeshService(private val context: Context) { // Packet operations override fun sendPacket(packet: BitchatPacket) { - connectionManager.broadcastPacket(packet) + connectionManager.broadcastPacket(RoutedPacket(packet)) } - override fun relayPacket(packet: BitchatPacket) { - connectionManager.broadcastPacket(packet) + override fun relayPacket(routed: RoutedPacket) { + connectionManager.broadcastPacket(routed) } override fun getBroadcastRecipient(): ByteArray { @@ -221,32 +226,32 @@ class BluetoothMeshService(private val context: Context) { peerManager.updatePeerLastSeen(peerID) } - override fun handleKeyExchange(packet: BitchatPacket, peerID: String): Boolean { - return runBlocking { securityManager.handleKeyExchange(packet, peerID) } + override fun handleKeyExchange(routed: RoutedPacket): Boolean { + return runBlocking { securityManager.handleKeyExchange(routed) } } - override fun handleAnnounce(packet: BitchatPacket, peerID: String) { - serviceScope.launch { messageHandler.handleAnnounce(packet, peerID) } + override fun handleAnnounce(routed: RoutedPacket) { + serviceScope.launch { messageHandler.handleAnnounce(routed) } } - override fun handleMessage(packet: BitchatPacket, peerID: String) { - serviceScope.launch { messageHandler.handleMessage(packet, peerID) } + override fun handleMessage(routed: RoutedPacket) { + serviceScope.launch { messageHandler.handleMessage(routed) } } - override fun handleLeave(packet: BitchatPacket, peerID: String) { - serviceScope.launch { messageHandler.handleLeave(packet, peerID) } + override fun handleLeave(routed: RoutedPacket) { + serviceScope.launch { messageHandler.handleLeave(routed) } } override fun handleFragment(packet: BitchatPacket): BitchatPacket? { return fragmentManager.handleFragment(packet) } - override fun handleDeliveryAck(packet: BitchatPacket, peerID: String) { - serviceScope.launch { messageHandler.handleDeliveryAck(packet, peerID) } + override fun handleDeliveryAck(routed: RoutedPacket) { + serviceScope.launch { messageHandler.handleDeliveryAck(routed) } } - override fun handleReadReceipt(packet: BitchatPacket, peerID: String) { - serviceScope.launch { messageHandler.handleReadReceipt(packet, peerID) } + override fun handleReadReceipt(routed: RoutedPacket) { + serviceScope.launch { messageHandler.handleReadReceipt(routed) } } override fun sendAnnouncementToPeer(peerID: String) { @@ -257,15 +262,15 @@ class BluetoothMeshService(private val context: Context) { storeForwardManager.sendCachedMessages(peerID) } - override fun relayPacket(packet: BitchatPacket) { - connectionManager.broadcastPacket(packet) + override fun relayPacket(routed: RoutedPacket) { + connectionManager.broadcastPacket(routed) } } // BluetoothConnectionManager delegates connectionManager.delegate = object : BluetoothConnectionManagerDelegate { override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: android.bluetooth.BluetoothDevice?) { - packetProcessor.processPacket(packet, peerID) + packetProcessor.processPacket(RoutedPacket(packet, peerID, device?.address)) } override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) { @@ -372,7 +377,7 @@ class BluetoothMeshService(private val context: Context) { // Send with random delay and retry for reliability // delay(Random.nextLong(50, 500)) - connectionManager.broadcastPacket(packet) + connectionManager.broadcastPacket(RoutedPacket(packet)) } } } @@ -425,7 +430,7 @@ class BluetoothMeshService(private val context: Context) { // Send with delay delay(Random.nextLong(50, 500)) - connectionManager.broadcastPacket(packet) + connectionManager.broadcastPacket(RoutedPacket(packet)) } } catch (e: Exception) { @@ -451,13 +456,13 @@ class BluetoothMeshService(private val context: Context) { // Send multiple times for reliability delay(Random.nextLong(0, 500)) - connectionManager.broadcastPacket(announcePacket) + connectionManager.broadcastPacket(RoutedPacket(announcePacket)) delay(500 + Random.nextLong(0, 500)) - connectionManager.broadcastPacket(announcePacket) + connectionManager.broadcastPacket(RoutedPacket(announcePacket)) delay(1000 + Random.nextLong(0, 500)) - connectionManager.broadcastPacket(announcePacket) + connectionManager.broadcastPacket(RoutedPacket(announcePacket)) } } @@ -475,7 +480,7 @@ class BluetoothMeshService(private val context: Context) { payload = nickname.toByteArray() ) - connectionManager.broadcastPacket(packet) + connectionManager.broadcastPacket(RoutedPacket(packet)) peerManager.markPeerAsAnnouncedTo(peerID) } @@ -491,7 +496,7 @@ class BluetoothMeshService(private val context: Context) { payload = publicKeyData ) - connectionManager.broadcastPacket(packet) + connectionManager.broadcastPacket(RoutedPacket(packet)) Log.d(TAG, "Sent key exchange") } @@ -507,7 +512,7 @@ class BluetoothMeshService(private val context: Context) { payload = nickname.toByteArray() ) - connectionManager.broadcastPacket(packet) + connectionManager.broadcastPacket(RoutedPacket(packet)) } /** diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index d25caf1c..2cff0e6c 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -5,6 +5,7 @@ import com.bitchat.android.crypto.MessagePadding import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.DeliveryAck import com.bitchat.android.model.ReadReceipt +import com.bitchat.android.model.RoutedPacket import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType import kotlinx.coroutines.* @@ -30,7 +31,10 @@ class MessageHandler(private val myPeerID: String) { /** * Handle announce message */ - suspend fun handleAnnounce(packet: BitchatPacket, peerID: String): Boolean { + suspend fun handleAnnounce(routed: RoutedPacket): Boolean { + val packet = routed.packet + val peerID = routed.peerID ?: "unknown" + if (peerID == myPeerID) return false val nickname = String(packet.payload, Charsets.UTF_8) @@ -43,7 +47,7 @@ class MessageHandler(private val myPeerID: String) { if (packet.ttl > 1u) { val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) delay(Random.nextLong(100, 300)) - delegate?.relayPacket(relayPacket) + delegate?.relayPacket(RoutedPacket(relayPacket, peerID, routed.relayAddress)) } return isFirstAnnounce @@ -52,27 +56,31 @@ class MessageHandler(private val myPeerID: String) { /** * Handle broadcast or private message */ - suspend fun handleMessage(packet: BitchatPacket, peerID: String) { + suspend fun handleMessage(routed: RoutedPacket) { + val packet = routed.packet + val peerID = routed.peerID ?: "unknown" if (peerID == myPeerID) return val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) } if (recipientID == null) { // BROADCAST MESSAGE - handleBroadcastMessage(packet, peerID) + handleBroadcastMessage(routed) } else if (String(recipientID).replace("\u0000", "") == myPeerID) { // PRIVATE MESSAGE FOR US handlePrivateMessage(packet, peerID) } else if (packet.ttl > 0u) { // RELAY MESSAGE - relayMessage(packet) + relayMessage(routed) } } /** * Handle broadcast message */ - private suspend fun handleBroadcastMessage(packet: BitchatPacket, peerID: String) { + private suspend fun handleBroadcastMessage(routed: RoutedPacket) { + val packet = routed.packet + val peerID = routed.peerID ?: "unknown" try { // Parse message val message = BitchatMessage.fromBinaryPayload(packet.payload) @@ -104,7 +112,7 @@ class MessageHandler(private val myPeerID: String) { } // Relay broadcast messages - relayMessage(packet) + relayMessage(routed) } catch (e: Exception) { Log.e(TAG, "Failed to process broadcast message: ${e.message}") @@ -162,7 +170,9 @@ class MessageHandler(private val myPeerID: String) { /** * Handle leave message */ - suspend fun handleLeave(packet: BitchatPacket, peerID: String) { + suspend fun handleLeave(routed: RoutedPacket) { + val packet = routed.packet + val peerID = routed.peerID ?: "unknown" val content = String(packet.payload, Charsets.UTF_8) if (content.startsWith("#")) { @@ -180,14 +190,16 @@ class MessageHandler(private val myPeerID: String) { // Relay if TTL > 0 if (packet.ttl > 1u) { val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) - delegate?.relayPacket(relayPacket) + delegate?.relayPacket(routed.copy(packet = relayPacket)) } } /** * Handle delivery acknowledgment */ - suspend fun handleDeliveryAck(packet: BitchatPacket, peerID: String) { + suspend fun handleDeliveryAck(routed: RoutedPacket) { + val packet = routed.packet + val peerID = routed.peerID ?: "unknown" if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) { try { val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID) @@ -200,17 +212,19 @@ class MessageHandler(private val myPeerID: String) { } catch (e: Exception) { Log.e(TAG, "Failed to decrypt delivery ACK: ${e.message}") } - } else if (packet.ttl > 0u) { + } else if (packet.ttl > 0u && String(packet.senderID).replace("\u0000", "") != myPeerID) { // Relay val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) - delegate?.relayPacket(relayPacket) + delegate?.relayPacket(routed.copy(packet = relayPacket)) } } /** * Handle read receipt */ - suspend fun handleReadReceipt(packet: BitchatPacket, peerID: String) { + suspend fun handleReadReceipt(routed: RoutedPacket) { + val packet = routed.packet + val peerID = routed.peerID ?: "unknown" if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) { try { val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID) @@ -223,17 +237,18 @@ class MessageHandler(private val myPeerID: String) { } catch (e: Exception) { Log.e(TAG, "Failed to decrypt read receipt: ${e.message}") } - } else if (packet.ttl > 0u) { + } else if (packet.ttl > 0u && String(packet.senderID).replace("\u0000", "") != myPeerID) { // Relay val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) - delegate?.relayPacket(relayPacket) + delegate?.relayPacket(routed.copy(packet = relayPacket)) } } /** * Relay message with adaptive probability (same as iOS) */ - private suspend fun relayMessage(packet: BitchatPacket) { + private suspend fun relayMessage(routed: RoutedPacket) { + val packet = routed.packet if (packet.ttl == 0u.toUByte()) return val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) @@ -253,7 +268,7 @@ class MessageHandler(private val myPeerID: String) { if (shouldRelay) { val delay = Random.nextLong(50, 500) // Random delay like iOS delay(delay) - delegate?.relayPacket(relayPacket) + delegate?.relayPacket(routed.copy(packet = relayPacket)) } } @@ -326,7 +341,7 @@ interface MessageHandlerDelegate { // Packet operations fun sendPacket(packet: BitchatPacket) - fun relayPacket(packet: BitchatPacket) + fun relayPacket(routed: RoutedPacket) fun getBroadcastRecipient(): ByteArray // Cryptographic operations diff --git a/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt index 35084d27..dc43ed7a 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt @@ -3,6 +3,7 @@ package com.bitchat.android.mesh import android.util.Log import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType +import com.bitchat.android.model.RoutedPacket import kotlinx.coroutines.* /** @@ -24,16 +25,19 @@ class PacketProcessor(private val myPeerID: String) { /** * Process received packet - main entry point for all incoming packets */ - fun processPacket(packet: BitchatPacket, peerID: String) { + fun processPacket(routed: RoutedPacket) { processorScope.launch { - handleReceivedPacket(packet, peerID) + handleReceivedPacket(routed) } } /** * Handle received packet - core protocol logic (exact same as iOS) */ - private suspend fun handleReceivedPacket(packet: BitchatPacket, peerID: String) { + private suspend fun handleReceivedPacket(routed: RoutedPacket) { + val packet = routed.packet + val peerID = routed.peerID ?: "unknown" + // Basic validation and security checks if (!delegate?.validatePacketSecurity(packet, peerID)!!) { Log.d(TAG, "Packet failed security validation from $peerID") @@ -47,15 +51,15 @@ class PacketProcessor(private val myPeerID: String) { // Process based on message type (exact same logic as iOS) when (MessageType.fromValue(packet.type)) { - MessageType.KEY_EXCHANGE -> handleKeyExchange(packet, peerID) - MessageType.ANNOUNCE -> handleAnnounce(packet, peerID) - MessageType.MESSAGE -> handleMessage(packet, peerID) - MessageType.LEAVE -> handleLeave(packet, peerID) + MessageType.KEY_EXCHANGE -> handleKeyExchange(routed) + MessageType.ANNOUNCE -> handleAnnounce(routed) + MessageType.MESSAGE -> handleMessage(routed) + MessageType.LEAVE -> handleLeave(routed) MessageType.FRAGMENT_START, MessageType.FRAGMENT_CONTINUE, - MessageType.FRAGMENT_END -> handleFragment(packet, peerID) - MessageType.DELIVERY_ACK -> handleDeliveryAck(packet, peerID) - MessageType.READ_RECEIPT -> handleReadReceipt(packet, peerID) + MessageType.FRAGMENT_END -> handleFragment(routed) + MessageType.DELIVERY_ACK -> handleDeliveryAck(routed) + MessageType.READ_RECEIPT -> handleReadReceipt(routed) else -> { Log.w(TAG, "Unknown message type: ${packet.type}") } @@ -65,10 +69,11 @@ class PacketProcessor(private val myPeerID: String) { /** * Handle key exchange message */ - private suspend fun handleKeyExchange(packet: BitchatPacket, peerID: String) { + private suspend fun handleKeyExchange(routed: RoutedPacket) { + val peerID = routed.peerID ?: "unknown" Log.d(TAG, "Processing key exchange from $peerID") - val success = delegate?.handleKeyExchange(packet, peerID) ?: false + val success = delegate?.handleKeyExchange(routed) ?: false if (success) { // Key exchange successful, send announce and cached messages @@ -83,60 +88,60 @@ class PacketProcessor(private val myPeerID: String) { /** * Handle announce message */ - private suspend fun handleAnnounce(packet: BitchatPacket, peerID: String) { - Log.d(TAG, "Processing announce from $peerID") - delegate?.handleAnnounce(packet, peerID) + private suspend fun handleAnnounce(routed: RoutedPacket) { + Log.d(TAG, "Processing announce from ${routed.peerID}") + delegate?.handleAnnounce(routed) } /** * Handle regular message */ - private suspend fun handleMessage(packet: BitchatPacket, peerID: String) { - Log.d(TAG, "Processing message from $peerID") - delegate?.handleMessage(packet, peerID) + private suspend fun handleMessage(routed: RoutedPacket) { + Log.d(TAG, "Processing message from ${routed.peerID}") + delegate?.handleMessage(routed) } /** * Handle leave message */ - private suspend fun handleLeave(packet: BitchatPacket, peerID: String) { - Log.d(TAG, "Processing leave from $peerID") - delegate?.handleLeave(packet, peerID) + private suspend fun handleLeave(routed: RoutedPacket) { + Log.d(TAG, "Processing leave from ${routed.peerID}") + delegate?.handleLeave(routed) } /** * Handle message fragments */ - private suspend fun handleFragment(packet: BitchatPacket, peerID: String) { - Log.d(TAG, "Processing fragment from $peerID") + private suspend fun handleFragment(routed: RoutedPacket) { + Log.d(TAG, "Processing fragment from ${routed.peerID}") - val reassembledPacket = delegate?.handleFragment(packet) + val reassembledPacket = delegate?.handleFragment(routed.packet) if (reassembledPacket != null) { Log.d(TAG, "Fragment reassembled, processing complete message") - handleReceivedPacket(reassembledPacket, peerID) + handleReceivedPacket(RoutedPacket(reassembledPacket, routed.peerID, routed.relayAddress)) } // Relay fragment regardless of reassembly - if (packet.ttl > 0u) { - val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) - delegate?.relayPacket(relayPacket) + if (routed.packet.ttl > 0u) { + val relayPacket = routed.packet.copy(ttl = (routed.packet.ttl - 1u).toUByte()) + delegate?.relayPacket(RoutedPacket(relayPacket, routed.peerID, routed.relayAddress)) } } /** * Handle delivery acknowledgment */ - private suspend fun handleDeliveryAck(packet: BitchatPacket, peerID: String) { - Log.d(TAG, "Processing delivery ACK from $peerID") - delegate?.handleDeliveryAck(packet, peerID) + private suspend fun handleDeliveryAck(routed: RoutedPacket) { + Log.d(TAG, "Processing delivery ACK from ${routed.peerID}") + delegate?.handleDeliveryAck(routed) } /** * Handle read receipt */ - private suspend fun handleReadReceipt(packet: BitchatPacket, peerID: String) { - Log.d(TAG, "Processing read receipt from $peerID") - delegate?.handleReadReceipt(packet, peerID) + private suspend fun handleReadReceipt(routed: RoutedPacket) { + Log.d(TAG, "Processing read receipt from ${routed.peerID}") + delegate?.handleReadReceipt(routed) } /** @@ -169,16 +174,16 @@ interface PacketProcessorDelegate { fun updatePeerLastSeen(peerID: String) // Message type handlers - fun handleKeyExchange(packet: BitchatPacket, peerID: String): Boolean - fun handleAnnounce(packet: BitchatPacket, peerID: String) - fun handleMessage(packet: BitchatPacket, peerID: String) - fun handleLeave(packet: BitchatPacket, peerID: String) + fun handleKeyExchange(routed: RoutedPacket): Boolean + fun handleAnnounce(routed: RoutedPacket) + fun handleMessage(routed: RoutedPacket) + fun handleLeave(routed: RoutedPacket) fun handleFragment(packet: BitchatPacket): BitchatPacket? - fun handleDeliveryAck(packet: BitchatPacket, peerID: String) - fun handleReadReceipt(packet: BitchatPacket, peerID: String) + fun handleDeliveryAck(routed: RoutedPacket) + fun handleReadReceipt(routed: RoutedPacket) // Communication fun sendAnnouncementToPeer(peerID: String) fun sendCachedMessages(peerID: String) - fun relayPacket(packet: BitchatPacket) + fun relayPacket(routed: RoutedPacket) } diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index 65b74e42..50c0ce92 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -4,6 +4,7 @@ import android.util.Log import com.bitchat.android.crypto.EncryptionService import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType +import com.bitchat.android.model.RoutedPacket import kotlinx.coroutines.* import java.util.* import kotlin.collections.mutableSetOf @@ -88,7 +89,10 @@ class SecurityManager(private val encryptionService: EncryptionService, private /** * Handle key exchange packet */ - suspend fun handleKeyExchange(packet: BitchatPacket, peerID: String): Boolean { + suspend fun handleKeyExchange(routed: RoutedPacket): Boolean { + val packet = routed.packet + val peerID = routed.peerID ?: "unknown" + if (peerID == myPeerID) return false if (packet.payload.isEmpty()) { @@ -113,7 +117,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private Log.d(TAG, "Successfully processed key exchange from $peerID") // Notify delegate - delegate?.onKeyExchangeCompleted(peerID, packet.payload) + delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress) return true @@ -315,5 +319,5 @@ class SecurityManager(private val encryptionService: EncryptionService, private * Delegate interface for security manager callbacks */ interface SecurityManagerDelegate { - fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) + fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?) } diff --git a/app/src/main/java/com/bitchat/android/model/RoutedPacket.kt b/app/src/main/java/com/bitchat/android/model/RoutedPacket.kt new file mode 100644 index 00000000..59697360 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/model/RoutedPacket.kt @@ -0,0 +1,13 @@ +package com.bitchat.android.model + +import com.bitchat.android.protocol.BitchatPacket + +/** + * Represents a routed packet with additional metadata + * Used for processing and routing packets in the mesh network + */ +data class RoutedPacket( + val packet: BitchatPacket, + val peerID: String? = null, // Who sent it (parsed from packet.senderID) + val relayAddress: String? = null // Address it came from (for avoiding loopback) +) \ No newline at end of file From 1b37f46d1e647e5ebddc2153ddee3993cea7383a Mon Sep 17 00:00:00 2001 From: GUVWAF Date: Fri, 11 Jul 2025 16:39:47 +0200 Subject: [PATCH 24/41] Fallback to broadcast if direct send fails --- .../mesh/BluetoothConnectionManager.kt | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index e729496a..87655ef8 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -203,35 +203,39 @@ class BluetoothConnectionManager( } // Function to send data to a single device (server side) - private fun notifyDevice(device: BluetoothDevice, data: ByteArray) { - try { + private fun notifyDevice(device: BluetoothDevice, data: ByteArray): Boolean { + return try { characteristic?.let { char -> char.value = data - gattServer?.notifyCharacteristicChanged(device, char, false) - } + val result = gattServer?.notifyCharacteristicChanged(device, char, false) ?: false + result + } ?: false } catch (e: Exception) { Log.w(TAG, "Error sending to server connection ${device.address}: ${e.message}") connectionScope.launch { delay(CLEANUP_DELAY) subscribedDevices.remove(device) - addressPeerMap.remove(device.getAddress()) + addressPeerMap.remove(device.address) } + false } } // Function to send data to a single device (client side) - private fun writeToDeviceConn(deviceConn: DeviceConnection, data: ByteArray) { - try { + private fun writeToDeviceConn(deviceConn: DeviceConnection, data: ByteArray): Boolean { + return try { deviceConn.characteristic?.let { char -> char.value = data - deviceConn.gatt?.writeCharacteristic(char) - } + val result = deviceConn.gatt?.writeCharacteristic(char) ?: false + result + } ?: false } catch (e: Exception) { Log.w(TAG, "Error sending to client connection ${deviceConn.device.address}: ${e.message}") connectionScope.launch { delay(CLEANUP_DELAY) cleanupDeviceConnection(deviceConn.device.address) } + false } } @@ -255,8 +259,8 @@ class BluetoothConnectionManager( // If found, send directly if (targetDevice != null) { Log.d(TAG, "Send packet type ${packet.type} directly to target device for recipient $recipientID: ${targetDevice.address}") - notifyDevice(targetDevice, data) - return // Sent, no need to continue + if (notifyDevice(targetDevice, data)) + return // Sent, no need to continue } // Try to find the recipient in client connections (connectedDevices) @@ -264,8 +268,8 @@ class BluetoothConnectionManager( // If found, send directly if (targetDeviceConn != null) { Log.d(TAG, "Send packet type ${packet.type} directly to target client connection for recipient $recipientID: ${targetDeviceConn.device.address}") - writeToDeviceConn(targetDeviceConn, data) - return // Sent, no need to continue + if (writeToDeviceConn(targetDeviceConn, data)) + return // Sent, no need to continue } } From 035273011fbf1bf14ce75edc70d0c99ce32da51b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 15:42:37 +0200 Subject: [PATCH 25/41] fix startup --- .../java/com/bitchat/android/MainActivity.kt | 54 ++++- .../mesh/BluetoothConnectionManager.kt | 83 ++++--- .../android/mesh/BluetoothMeshService.kt | 14 +- .../services/MessageRetentionService.kt | 219 ++++++++++++++++++ .../com/bitchat/android/ui/ChatViewModel.kt | 37 +-- 5 files changed, 325 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index a1f5390c..e07873e8 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -17,6 +17,8 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.ViewModelProvider +import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.onboarding.* import com.bitchat.android.ui.ChatScreen import com.bitchat.android.ui.ChatViewModel @@ -29,7 +31,17 @@ class MainActivity : ComponentActivity() { private lateinit var permissionManager: PermissionManager private lateinit var onboardingCoordinator: OnboardingCoordinator private lateinit var bluetoothStatusManager: BluetoothStatusManager - private val chatViewModel: ChatViewModel by viewModels() + + // Core mesh service - managed at app level + private lateinit var meshService: BluetoothMeshService + private val chatViewModel: ChatViewModel by viewModels { + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return ChatViewModel(application, meshService) as T + } + } + } // UI state for onboarding flow private var onboardingState by mutableStateOf(OnboardingState.CHECKING) @@ -50,6 +62,9 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Initialize core mesh service first + meshService = BluetoothMeshService(this) + // Initialize permission management permissionManager = PermissionManager(this) bluetoothStatusManager = BluetoothStatusManager( @@ -289,7 +304,7 @@ class MainActivity : ComponentActivity() { // This solves the issue where app needs restart to work on first install delay(1000) // Give the system time to process permission grants - android.util.Log.d("MainActivity", "Permissions verified, starting mesh service") + android.util.Log.d("MainActivity", "Permissions verified, initializing chat system") // Ensure all permissions are still granted (user might have revoked in settings) if (!permissionManager.areAllPermissionsGranted()) { @@ -299,8 +314,11 @@ class MainActivity : ComponentActivity() { return@launch } - // Initialize chat view model - this will start the mesh service - chatViewModel.meshService.startServices() + // Set up mesh service delegate and start services + meshService.delegate = chatViewModel + meshService.startServices() + + android.util.Log.d("MainActivity", "Mesh service started successfully") // Handle any notification intent handleNotificationIntent(intent) @@ -330,6 +348,8 @@ class MainActivity : ComponentActivity() { super.onResume() // Check Bluetooth status on resume and handle accordingly if (onboardingState == OnboardingState.COMPLETE) { + // Set app foreground state + meshService.connectionManager.setAppBackgroundState(false) chatViewModel.setAppBackgroundState(false) // Check if Bluetooth was disabled while app was backgrounded @@ -347,6 +367,8 @@ class MainActivity : ComponentActivity() { super.onPause() // Only set background state if app is fully initialized if (onboardingState == OnboardingState.COMPLETE) { + // Set app background state + meshService.connectionManager.setAppBackgroundState(true) chatViewModel.setAppBackgroundState(true) } } @@ -376,12 +398,32 @@ class MainActivity : ComponentActivity() { } } + /** + * Restart mesh services (for debugging/troubleshooting) + */ + fun restartMeshServices() { + if (onboardingState == OnboardingState.COMPLETE) { + lifecycleScope.launch { + try { + android.util.Log.d("MainActivity", "Restarting mesh services") + meshService.stopServices() + delay(1000) + meshService.startServices() + android.util.Log.d("MainActivity", "Mesh services restarted successfully") + } catch (e: Exception) { + android.util.Log.e("MainActivity", "Error restarting mesh services: ${e.message}") + } + } + } + } + override fun onDestroy() { super.onDestroy() - // Only stop mesh services if they were started + // Stop mesh services if app was fully initialized if (onboardingState == OnboardingState.COMPLETE) { try { - chatViewModel.meshService.stopServices() + meshService.stopServices() + android.util.Log.d("MainActivity", "Mesh services stopped successfully") } catch (e: Exception) { android.util.Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}") } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index 83673d1f..060bef86 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -129,16 +129,16 @@ class BluetoothConnectionManager( try { isActive = true + // Setup GATT server first + setupGattServer() + // Start power manager and services connectionScope.launch { powerManager.start() - - // Setup GATT server after power manager is ready - setupGattServer() - delay(500) // Ensure GATT server is ready + delay(300) // Brief delay to ensure GATT server is ready startAdvertising() - delay(200) + delay(100) if (powerManager.shouldUseDutyCycle()) { Log.i(TAG, "Using power-aware duty cycling") @@ -458,48 +458,47 @@ class BluetoothConnectionManager( // Proper cleanup sequencing to prevent race conditions gattServer?.let { server -> Log.d(TAG, "Cleaning up existing GATT server") - connectionScope.launch { - // Give time for pending callbacks to complete - delay(100) + try { server.close() + } catch (e: Exception) { + Log.w(TAG, "Error closing existing GATT server: ${e.message}") } } - // Create new server after cleanup delay - connectionScope.launch { - delay(200) // Allow previous server to fully close - - if (!isActive) { - Log.d(TAG, "Service inactive, skipping GATT server creation") - return@launch - } - - gattServer = bluetoothManager.openGattServer(context, serverCallback) - - // Create characteristic with notification support - characteristic = BluetoothGattCharacteristic( - CHARACTERISTIC_UUID, - BluetoothGattCharacteristic.PROPERTY_READ or - BluetoothGattCharacteristic.PROPERTY_WRITE or - BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or - BluetoothGattCharacteristic.PROPERTY_NOTIFY, - BluetoothGattCharacteristic.PERMISSION_READ or - BluetoothGattCharacteristic.PERMISSION_WRITE - ) - - val descriptor = BluetoothGattDescriptor( - UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), - BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE - ) - characteristic?.addDescriptor(descriptor) - - val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) - service.addCharacteristic(characteristic) - - gattServer?.addService(service) - - Log.i(TAG, "GATT server setup complete") + // Small delay to ensure cleanup is complete + Thread.sleep(100) + + if (!isActive) { + Log.d(TAG, "Service inactive, skipping GATT server creation") + return } + + // Create new server + gattServer = bluetoothManager.openGattServer(context, serverCallback) + + // Create characteristic with notification support + characteristic = BluetoothGattCharacteristic( + CHARACTERISTIC_UUID, + BluetoothGattCharacteristic.PROPERTY_READ or + BluetoothGattCharacteristic.PROPERTY_WRITE or + BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or + BluetoothGattCharacteristic.PROPERTY_NOTIFY, + BluetoothGattCharacteristic.PERMISSION_READ or + BluetoothGattCharacteristic.PERMISSION_WRITE + ) + + val descriptor = BluetoothGattDescriptor( + UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), + BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE + ) + characteristic?.addDescriptor(descriptor) + + val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) + service.addCharacteristic(characteristic) + + gattServer?.addService(service) + + Log.i(TAG, "GATT server setup complete") } @Suppress("DEPRECATION") diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index e92a6dfc..7732fd0f 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -69,10 +69,10 @@ class BluetoothMeshService(private val context: Context) { while (isActive) { try { delay(10000) // 10 seconds - val debugInfo = getDebugStatus() - Log.d(TAG, "=== PERIODIC DEBUG STATUS ===") - Log.d(TAG, debugInfo) - Log.d(TAG, "=== END DEBUG STATUS ===") + if (isActive) { // Double-check before logging + val debugInfo = getDebugStatus() + Log.d(TAG, "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ===") + } } catch (e: Exception) { Log.e(TAG, "Error in periodic debug logging: ${e.message}") } @@ -282,7 +282,7 @@ class BluetoothMeshService(private val context: Context) { * Start the mesh service */ fun startServices() { - // Prevent double starts + // Prevent double starts (defensive programming) if (isActive) { Log.w(TAG, "Mesh service already active, ignoring duplicate start request") return @@ -297,9 +297,7 @@ class BluetoothMeshService(private val context: Context) { // Send initial announcements after services are ready serviceScope.launch { delay(1000) - if (isActive) { // Check if still active - sendBroadcastAnnounce() - } + sendBroadcastAnnounce() } } else { Log.e(TAG, "Failed to start Bluetooth services") diff --git a/app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt b/app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt new file mode 100644 index 00000000..a0651850 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt @@ -0,0 +1,219 @@ +package com.bitchat.android.services + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import com.bitchat.android.model.BitchatMessage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.util.* + +/** + * Message retention service for saving channel messages locally + * Matches iOS MessageRetentionService functionality + */ +class MessageRetentionService private constructor(private val context: Context) { + + companion object { + private const val TAG = "MessageRetentionService" + private const val PREF_NAME = "message_retention" + private const val KEY_FAVORITE_CHANNELS = "favorite_channels" + + @Volatile + private var INSTANCE: MessageRetentionService? = null + + fun getInstance(context: Context): MessageRetentionService { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: MessageRetentionService(context.applicationContext).also { INSTANCE = it } + } + } + } + + private val prefs: SharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + private val retentionDir = File(context.filesDir, "retained_messages") + + init { + if (!retentionDir.exists()) { + retentionDir.mkdirs() + } + } + + // MARK: - Channel Bookmarking (Favorites) + + fun getFavoriteChannels(): Set { + return prefs.getStringSet(KEY_FAVORITE_CHANNELS, emptySet()) ?: emptySet() + } + + fun toggleFavoriteChannel(channel: String): Boolean { + val currentFavorites = getFavoriteChannels().toMutableSet() + val wasAdded = if (currentFavorites.contains(channel)) { + currentFavorites.remove(channel) + false + } else { + currentFavorites.add(channel) + true + } + + prefs.edit().putStringSet(KEY_FAVORITE_CHANNELS, currentFavorites).apply() + + if (!wasAdded) { + // Channel removed from favorites - delete saved messages in background + Thread { + try { + val channelFile = getChannelFile(channel) + if (channelFile.exists()) { + channelFile.delete() + Log.d(TAG, "Deleted saved messages for channel $channel") + } + } catch (e: Exception) { + Log.e(TAG, "Failed to delete messages for channel $channel", e) + } + }.start() + } + + Log.d(TAG, "Channel $channel ${if (wasAdded) "bookmarked" else "unbookmarked"}") + return wasAdded + } + + fun isChannelBookmarked(channel: String): Boolean { + return getFavoriteChannels().contains(channel) + } + + // MARK: - Message Storage + + suspend fun saveMessage(message: BitchatMessage, forChannel: String) = withContext(Dispatchers.IO) { + if (!isChannelBookmarked(forChannel)) { + Log.w(TAG, "Attempted to save message for non-bookmarked channel: $forChannel") + return@withContext + } + + try { + val channelFile = getChannelFile(forChannel) + val existingMessages = loadMessagesFromFile(channelFile).toMutableList() + + // Check if message already exists (by ID) + if (existingMessages.any { it.id == message.id }) { + Log.d(TAG, "Message ${message.id} already saved for channel $forChannel") + return@withContext + } + + // Add new message + existingMessages.add(message) + + // Sort by timestamp + existingMessages.sortBy { it.timestamp } + + // Save back to file + saveMessagesToFile(channelFile, existingMessages) + + Log.d(TAG, "Saved message ${message.id} for channel $forChannel") + + } catch (e: Exception) { + Log.e(TAG, "Failed to save message for channel $forChannel", e) + } + } + + suspend fun loadMessagesForChannel(channel: String): List = withContext(Dispatchers.IO) { + if (!isChannelBookmarked(channel)) { + Log.d(TAG, "Channel $channel not bookmarked, returning empty list") + return@withContext emptyList() + } + + try { + val channelFile = getChannelFile(channel) + val messages = loadMessagesFromFile(channelFile) + Log.d(TAG, "Loaded ${messages.size} messages for channel $channel") + return@withContext messages + } catch (e: Exception) { + Log.e(TAG, "Failed to load messages for channel $channel", e) + return@withContext emptyList() + } + } + + suspend fun deleteMessagesForChannel(channel: String): Unit = withContext(Dispatchers.IO) { + try { + val channelFile = getChannelFile(channel) + if (channelFile.exists()) { + channelFile.delete() + Log.d(TAG, "Deleted saved messages for channel $channel") + } + } catch (e: Exception) { + Log.e(TAG, "Failed to delete messages for channel $channel", e) + } + } + + suspend fun deleteAllStoredMessages(): Unit = withContext(Dispatchers.IO) { + try { + if (retentionDir.exists()) { + retentionDir.listFiles()?.forEach { file -> + file.delete() + } + Log.d(TAG, "Deleted all stored messages") + } + } catch (e: Exception) { + Log.e(TAG, "Failed to delete all stored messages", e) + } + } + + // MARK: - File Operations + + private fun getChannelFile(channel: String): File { + // Sanitize channel name for filename + val sanitizedChannel = channel.replace("[^a-zA-Z0-9_-]".toRegex(), "_") + return File(retentionDir, "channel_${sanitizedChannel}.dat") + } + + private fun loadMessagesFromFile(file: File): List { + if (!file.exists()) { + return emptyList() + } + + return try { + FileInputStream(file).use { fis -> + ObjectInputStream(fis).use { ois -> + @Suppress("UNCHECKED_CAST") + ois.readObject() as List + } + } + } catch (e: Exception) { + Log.w(TAG, "Failed to load messages from ${file.name}, returning empty list", e) + emptyList() + } + } + + private fun saveMessagesToFile(file: File, messages: List) { + FileOutputStream(file).use { fos -> + ObjectOutputStream(fos).use { oos -> + oos.writeObject(messages) + } + } + } + + // MARK: - Statistics + + fun getBookmarkedChannelsCount(): Int { + return getFavoriteChannels().size + } + + suspend fun getTotalStoredMessagesCount(): Int = withContext(Dispatchers.IO) { + var totalCount = 0 + + try { + retentionDir.listFiles()?.forEach { file -> + if (file.name.startsWith("channel_") && file.name.endsWith(".dat")) { + val messages = loadMessagesFromFile(file) + totalCount += messages.size + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to count stored messages", e) + } + + totalCount + } +} diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 4240c1cf..66502fd6 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -19,13 +19,13 @@ import kotlin.random.Random * Refactored ChatViewModel - Main coordinator for bitchat functionality * Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility */ -class ChatViewModel(application: Application) : AndroidViewModel(application), BluetoothMeshDelegate { +class ChatViewModel( + application: Application, + val meshService: BluetoothMeshService +) : AndroidViewModel(application), BluetoothMeshDelegate { private val context: Context = application.applicationContext - // Core services - val meshService = BluetoothMeshService(context) - // State management private val state = ChatState() @@ -72,7 +72,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B val favoritePeers: LiveData> = state.favoritePeers init { - meshService.delegate = this + // Note: Mesh service delegate is now set by MainActivity loadAndInitialize() } @@ -100,8 +100,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B state.setFavoritePeers(dataManager.favoritePeers) dataManager.loadBlockedUsers() - // Start mesh service - meshService.startServices() + // Note: Mesh service is now started by MainActivity // Show welcome message if no peers after delay viewModelScope.launch { @@ -120,7 +119,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B override fun onCleared() { super.onCleared() - meshService.stopServices() + // Note: Mesh service lifecycle is now managed by MainActivity } // MARK: - Nickname Management @@ -259,18 +258,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B return meshService.getDebugStatus() } - fun restartMeshServices() { - viewModelScope.launch { - meshService.stopServices() - delay(1000) - meshService.startServices() - } - } + // Note: Mesh service restart is now handled by MainActivity + // This function is no longer needed fun setAppBackgroundState(inBackground: Boolean) { - // Forward to connection manager for power optimization - meshService.connectionManager.setAppBackgroundState(inBackground) - // Forward to notification manager for notification logic notificationManager.setAppBackgroundState(inBackground) } @@ -355,13 +346,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B state.setNickname(newNickname) dataManager.saveNickname(newNickname) - // Disconnect from mesh - meshService.stopServices() - - // Restart services with new identity - viewModelScope.launch { - delay(500) - meshService.startServices() - } + // Note: Mesh service restart is now handled by MainActivity + // This method now only clears data, not mesh service lifecycle } } From d54d470614290b5c9ed5dd3b83c1cb6fd3181149 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 16:36:00 +0200 Subject: [PATCH 26/41] more logging --- .../bitchat/android/mesh/BluetoothConnectionManager.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index 060bef86..7fb2ce14 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -410,10 +410,15 @@ class BluetoothConnectionManager( } if (characteristic.uuid == CHARACTERISTIC_UUID) { + Log.d(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes") val packet = BitchatPacket.fromBinaryData(value) if (packet != null) { val peerID = String(packet.senderID).replace("\u0000", "") + Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID") delegate?.onPacketReceived(packet, peerID, device) + } else { + Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes") + Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") } if (responseNeeded) { @@ -835,10 +840,15 @@ class BluetoothConnectionManager( override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { val value = characteristic.value + Log.d(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes") val packet = BitchatPacket.fromBinaryData(value) if (packet != null) { val peerID = String(packet.senderID).replace("\u0000", "") + Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID") delegate?.onPacketReceived(packet, peerID, gatt.device) + } else { + Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes") + Log.w(TAG, "Client: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") } } } From ea25b71d6f06fad5f3a74b6818a08e91276249b4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 16:55:50 +0200 Subject: [PATCH 27/41] fix favorites --- .../java/com/bitchat/android/ui/ChatHeader.kt | 6 +++- .../java/com/bitchat/android/ui/ChatState.kt | 2 ++ .../com/bitchat/android/ui/ChatViewModel.kt | 17 +++++++++ .../com/bitchat/android/ui/DataManager.kt | 28 +++++++++++++-- .../bitchat/android/ui/PrivateChatManager.kt | 35 +++++++++++++++---- .../bitchat/android/ui/SidebarComponents.kt | 11 ++++-- 6 files changed, 87 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index d4372157..2916d8a3 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -1,5 +1,6 @@ package com.bitchat.android.ui +import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.BasicTextField @@ -253,7 +254,10 @@ private fun PrivateChatHeader( // Favorite button - positioned on the right IconButton( - onClick = onToggleFavorite, + onClick = { + Log.d("ChatHeader", "Header toggle favorite: peerID=$peerID, currentFavorite=$isFavorite") + onToggleFavorite() + }, modifier = Modifier.align(Alignment.CenterEnd) ) { Icon( diff --git a/app/src/main/java/com/bitchat/android/ui/ChatState.kt b/app/src/main/java/com/bitchat/android/ui/ChatState.kt index 9dc16cf6..7309d2fc 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatState.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatState.kt @@ -1,5 +1,6 @@ package com.bitchat.android.ui +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData @@ -188,6 +189,7 @@ class ChatState { } fun setFavoritePeers(favorites: Set) { + Log.d("ChatState", "setFavoritePeers called with ${favorites.size} favorites: $favorites") _favoritePeers.value = favorites } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 66502fd6..e8ad9c8d 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -2,6 +2,7 @@ package com.bitchat.android.ui import android.app.Application import android.content.Context +import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope @@ -100,6 +101,10 @@ class ChatViewModel( state.setFavoritePeers(dataManager.favoritePeers) dataManager.loadBlockedUsers() + // Log all favorites at startup + dataManager.logAllFavorites() + logCurrentFavoriteState() + // Note: Mesh service is now started by MainActivity // Show welcome message if no peers after delay @@ -249,7 +254,19 @@ class ChatViewModel( } fun toggleFavorite(peerID: String) { + Log.d("ChatViewModel", "toggleFavorite called for peerID: $peerID") privateChatManager.toggleFavorite(peerID) + + // Log current state after toggle + logCurrentFavoriteState() + } + + private fun logCurrentFavoriteState() { + Log.i("ChatViewModel", "=== CURRENT FAVORITE STATE ===") + Log.i("ChatViewModel", "LiveData favorite peers: ${favoritePeers.value}") + Log.i("ChatViewModel", "DataManager favorite peers: ${dataManager.favoritePeers}") + Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}") + Log.i("ChatViewModel", "==============================") } // MARK: - Debug and Troubleshooting diff --git a/app/src/main/java/com/bitchat/android/ui/DataManager.kt b/app/src/main/java/com/bitchat/android/ui/DataManager.kt index 73ffbcfe..0f16a1e0 100644 --- a/app/src/main/java/com/bitchat/android/ui/DataManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/DataManager.kt @@ -2,6 +2,7 @@ package com.bitchat.android.ui import android.content.Context import android.content.SharedPreferences +import android.util.Log import com.google.gson.Gson import kotlin.random.Random @@ -10,6 +11,10 @@ import kotlin.random.Random */ class DataManager(private val context: Context) { + companion object { + private const val TAG = "DataManager" + } + private val prefs: SharedPreferences = context.getSharedPreferences("bitchat_prefs", Context.MODE_PRIVATE) private val gson = Gson() @@ -126,24 +131,41 @@ class DataManager(private val context: Context) { fun loadFavorites() { val savedFavorites = prefs.getStringSet("favorites", emptySet()) ?: emptySet() _favoritePeers.addAll(savedFavorites) + Log.d(TAG, "Loaded ${savedFavorites.size} favorite users from storage: $savedFavorites") } fun saveFavorites() { prefs.edit().putStringSet("favorites", _favoritePeers).apply() + Log.d(TAG, "Saved ${_favoritePeers.size} favorite users to storage: $_favoritePeers") } fun addFavorite(fingerprint: String) { - _favoritePeers.add(fingerprint) + val wasAdded = _favoritePeers.add(fingerprint) + Log.d(TAG, "addFavorite: fingerprint=$fingerprint, wasAdded=$wasAdded") saveFavorites() + logAllFavorites() } fun removeFavorite(fingerprint: String) { - _favoritePeers.remove(fingerprint) + val wasRemoved = _favoritePeers.remove(fingerprint) + Log.d(TAG, "removeFavorite: fingerprint=$fingerprint, wasRemoved=$wasRemoved") saveFavorites() + logAllFavorites() } fun isFavorite(fingerprint: String): Boolean { - return _favoritePeers.contains(fingerprint) + val result = _favoritePeers.contains(fingerprint) + Log.d(TAG, "isFavorite check: fingerprint=$fingerprint, result=$result") + return result + } + + fun logAllFavorites() { + Log.i(TAG, "=== ALL FAVORITE USERS ===") + Log.i(TAG, "Total favorites: ${_favoritePeers.size}") + _favoritePeers.forEach { fingerprint -> + Log.i(TAG, "Favorite fingerprint: $fingerprint") + } + Log.i(TAG, "========================") } // MARK: - Blocked Users Management diff --git a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt index cb94ab4e..ceead27f 100644 --- a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt @@ -3,6 +3,7 @@ package com.bitchat.android.ui import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.DeliveryStatus import java.util.* +import android.util.Log /** * Handles private chat functionality including peer management and blocking @@ -13,6 +14,10 @@ class PrivateChatManager( private val dataManager: DataManager ) { + companion object { + private const val TAG = "PrivateChatManager" + } + // Peer identification mapping private val peerIDToPublicKeyFingerprint = mutableMapOf() @@ -99,17 +104,39 @@ class PrivateChatManager( fun toggleFavorite(peerID: String) { val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return - if (dataManager.isFavorite(fingerprint)) { + Log.d(TAG, "toggleFavorite called for peerID: $peerID, fingerprint: $fingerprint") + + val wasFavorite = dataManager.isFavorite(fingerprint) + Log.d(TAG, "Current favorite status: $wasFavorite") + + if (wasFavorite) { dataManager.removeFavorite(fingerprint) + Log.d(TAG, "Removed from favorites: $fingerprint") } else { dataManager.addFavorite(fingerprint) + Log.d(TAG, "Added to favorites: $fingerprint") } + + // Update state to trigger UI refresh state.setFavoritePeers(dataManager.favoritePeers) + + Log.d(TAG, "Updated favorite peers state. New favorites: ${dataManager.favoritePeers}") + Log.d(TAG, "All peer fingerprints: $peerIDToPublicKeyFingerprint") } fun isFavorite(peerID: String): Boolean { val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return false - return dataManager.isFavorite(fingerprint) + val isFav = dataManager.isFavorite(fingerprint) + Log.d(TAG, "isFavorite check: peerID=$peerID, fingerprint=$fingerprint, result=$isFav") + return isFav + } + + fun getPeerFingerprint(peerID: String): String? { + return peerIDToPublicKeyFingerprint[peerID] + } + + fun getPeerFingerprints(): Map { + return peerIDToPublicKeyFingerprint.toMap() } // MARK: - Block/Unblock Operations @@ -262,10 +289,6 @@ class PrivateChatManager( // MARK: - Public Getters - fun getPeerFingerprint(peerID: String): String? { - return peerIDToPublicKeyFingerprint[peerID] - } - fun getAllPeerFingerprints(): Map { return peerIDToPublicKeyFingerprint.toMap() } diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index a7ee2c4f..2bc7750d 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -1,5 +1,6 @@ package com.bitchat.android.ui +import android.util.Log import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.* @@ -275,16 +276,22 @@ fun PeopleSection( ) sortedPeers.forEach { peerID -> + val fingerprint = viewModel.privateChatManager.getPeerFingerprint(peerID) + val isFavorite = favoritePeers.contains(fingerprint) + PeerItem( peerID = peerID, displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: peerID), signalStrength = peerRSSI[peerID] ?: 0, isSelected = peerID == selectedPrivatePeer, - isFavorite = favoritePeers.contains(viewModel.privateChatManager.getPeerFingerprint(peerID)), + isFavorite = isFavorite, hasUnreadDM = hasUnreadPrivateMessages.contains(peerID), colorScheme = colorScheme, onItemClick = { onPrivateChatStart(peerID) }, - onToggleFavorite = { viewModel.toggleFavorite(peerID) } + onToggleFavorite = { + Log.d("SidebarComponents", "Sidebar toggle favorite: peerID=$peerID, fingerprint=$fingerprint, currentFavorite=$isFavorite") + viewModel.toggleFavorite(peerID) + } ) } } From a97e2549b8adf9abb340bca0c6b19a4716000d81 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:11:19 +0200 Subject: [PATCH 28/41] fix fav updates --- .../java/com/bitchat/android/ui/ChatHeader.kt | 10 ++++++++-- .../java/com/bitchat/android/ui/ChatState.kt | 9 +++++++++ .../bitchat/android/ui/PrivateChatManager.kt | 10 +++++++--- .../bitchat/android/ui/SidebarComponents.kt | 20 ++++++++++++------- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index 2916d8a3..65e2ee8e 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -152,11 +152,17 @@ fun ChatHeaderContent( when { selectedPrivatePeer != null -> { - // Private chat header + // Private chat header - ensure state synchronization + val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet()) + val fingerprint = viewModel.privateChatManager.getPeerFingerprint(selectedPrivatePeer) + val isFavorite = favoritePeers.contains(fingerprint) + + Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, fingerprint=$fingerprint, isFav=$isFavorite") + PrivateChatHeader( peerID = selectedPrivatePeer, peerNicknames = viewModel.meshService.getPeerNicknames(), - isFavorite = viewModel.isFavorite(selectedPrivatePeer), + isFavorite = isFavorite, onBackClick = onBackClick, onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) } ) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatState.kt b/app/src/main/java/com/bitchat/android/ui/ChatState.kt index 7309d2fc..748062b1 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatState.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatState.kt @@ -189,8 +189,17 @@ class ChatState { } fun setFavoritePeers(favorites: Set) { + val currentValue = _favoritePeers.value ?: emptySet() Log.d("ChatState", "setFavoritePeers called with ${favorites.size} favorites: $favorites") + Log.d("ChatState", "Current value: $currentValue") + Log.d("ChatState", "Values equal: ${currentValue == favorites}") + Log.d("ChatState", "Setting on thread: ${Thread.currentThread().name}") + + // Always set the value - even if equal, this ensures observers are triggered _favoritePeers.value = favorites + + Log.d("ChatState", "LiveData value after set: ${_favoritePeers.value}") + Log.d("ChatState", "LiveData has active observers: ${_favoritePeers.hasActiveObservers()}") } } diff --git a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt index ceead27f..6c70241e 100644 --- a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt @@ -109,6 +109,9 @@ class PrivateChatManager( val wasFavorite = dataManager.isFavorite(fingerprint) Log.d(TAG, "Current favorite status: $wasFavorite") + val currentFavorites = state.getFavoritePeersValue() + Log.d(TAG, "Current UI state favorites: $currentFavorites") + if (wasFavorite) { dataManager.removeFavorite(fingerprint) Log.d(TAG, "Removed from favorites: $fingerprint") @@ -117,10 +120,11 @@ class PrivateChatManager( Log.d(TAG, "Added to favorites: $fingerprint") } - // Update state to trigger UI refresh - state.setFavoritePeers(dataManager.favoritePeers) + // Always update state to trigger UI refresh - create new set to ensure change detection + val newFavorites = dataManager.favoritePeers.toSet() + state.setFavoritePeers(newFavorites) - Log.d(TAG, "Updated favorite peers state. New favorites: ${dataManager.favoritePeers}") + Log.d(TAG, "Force updated favorite peers state. New favorites: $newFavorites") Log.d(TAG, "All peer fingerprints: $peerIDToPublicKeyFingerprint") } diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index 2bc7750d..4186a76f 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -264,20 +264,26 @@ fun PeopleSection( val privateChats by viewModel.privateChats.observeAsState(emptyMap()) val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet()) + // Pre-calculate all favorite states to ensure proper state synchronization + val peerFavoriteStates = remember(favoritePeers, connectedPeers) { + connectedPeers.associateWith { peerID -> + val fingerprint = viewModel.privateChatManager.getPeerFingerprint(peerID) + favoritePeers.contains(fingerprint) + } + } + + Log.d("SidebarComponents", "Recomposing with ${favoritePeers.size} favorites, peer states: $peerFavoriteStates") + // Smart sorting: unread DMs first, then by most recent DM, then favorites, then alphabetical val sortedPeers = connectedPeers.sortedWith( compareBy { !hasUnreadPrivateMessages.contains(it) } // Unread DM senders first .thenByDescending { privateChats[it]?.maxByOrNull { msg -> msg.timestamp }?.timestamp?.time ?: 0L } // Most recent DM (convert Date to Long) - .thenBy { - val fingerprint = viewModel.privateChatManager.getPeerFingerprint(it) - fingerprint == null || !favoritePeers.contains(fingerprint) - } // Favorites + .thenBy { !(peerFavoriteStates[it] ?: false) } // Favorites first .thenBy { (if (it == nickname) "You" else (peerNicknames[it] ?: it)).lowercase() } // Alphabetical ) sortedPeers.forEach { peerID -> - val fingerprint = viewModel.privateChatManager.getPeerFingerprint(peerID) - val isFavorite = favoritePeers.contains(fingerprint) + val isFavorite = peerFavoriteStates[peerID] ?: false PeerItem( peerID = peerID, @@ -289,7 +295,7 @@ fun PeopleSection( colorScheme = colorScheme, onItemClick = { onPrivateChatStart(peerID) }, onToggleFavorite = { - Log.d("SidebarComponents", "Sidebar toggle favorite: peerID=$peerID, fingerprint=$fingerprint, currentFavorite=$isFavorite") + Log.d("SidebarComponents", "Sidebar toggle favorite: peerID=$peerID, currentFavorite=$isFavorite") viewModel.toggleFavorite(peerID) } ) From 8eab430022e58c83df93db3412de6dfa92a5aa82 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:24:11 +0200 Subject: [PATCH 29/41] bump to 0.6 --- CHANGELOG.md | 31 +++++++++++- CHAT_REFACTORING_PLAN.md | 64 ----------------------- REFACTORING_PROGRESS.md | 103 -------------------------------------- RSSI_COLOR_FIX_SUMMARY.md | 80 ----------------------------- app/build.gradle.kts | 4 +- demo_colors.md | 62 ----------------------- refactoring_plan.md | 73 --------------------------- 7 files changed, 32 insertions(+), 385 deletions(-) delete mode 100644 CHAT_REFACTORING_PLAN.md delete mode 100644 REFACTORING_PROGRESS.md delete mode 100644 RSSI_COLOR_FIX_SUMMARY.md delete mode 100644 demo_colors.md delete mode 100644 refactoring_plan.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c586165d..4a5ca76b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.6] + +### Added +- Channel password management with `/pass` command for channel owners +- Monochrome/themed launcher icon for Android 12+ dynamic theming support +- Unit tests package with initial testing infrastructure +- Production build optimization with code minification and shrinking ### Fixed +- Favorite peer functionality completely restored and improved + - Enhanced favorite system with fallback mechanism for peers without key exchange + - Fixed UI state updates for favorite stars in both header and sidebar + - Improved favorite persistence across app sessions - `/w` command now displays user nicknames instead of peer IDs +- Button styling and layout improvements across the app + - Enhanced back button positioning and styling + - Improved private chat and channel header button layouts + - Fixed button padding and alignment issues +- Color scheme consistency updates + - Updated orange color throughout the app to match iOS version + - Consistent color usage for private messages and UI elements +- App startup reliability improvements + - Better initialization sequence handling + - Fixed null pointer exceptions during startup + - Enhanced error handling and logging +- Input field styling and behavior improvements +- Sidebar user interaction enhancements +- Permission explanation screen layout fixes with proper vertical padding + +### Changed +- Updated GitHub organization references in project files +- Improved README documentation with updated clone URLs +- Enhanced logging throughout the application for better debugging ## [0.5.1] - 2025-07-10 diff --git a/CHAT_REFACTORING_PLAN.md b/CHAT_REFACTORING_PLAN.md deleted file mode 100644 index 934366cb..00000000 --- a/CHAT_REFACTORING_PLAN.md +++ /dev/null @@ -1,64 +0,0 @@ -# ChatScreen.kt Refactoring Plan - -## Current State -- Single file: `ChatScreen.kt` (~1,100+ lines) -- Multiple UI responsibilities mixed together -- Hard to maintain and test individual components - -## Proposed Component Structure - -### 1. Main Screen (ChatScreen.kt) -**Responsibilities:** -- Main layout orchestration -- State management delegation -- Window insets handling -- Component coordination - -### 2. Header Components (ChatHeader.kt) -**Responsibilities:** -- TopAppBar with different states (main, private, channel) -- Nickname editor -- Peer counter with status indicators -- Navigation controls - -### 3. Message Components (MessageComponents.kt) -**Responsibilities:** -- MessagesList composable -- MessageItem with formatting -- Message text parsing and styling -- Delivery status indicators -- RSSI-based coloring - -### 4. Input Components (InputComponents.kt) -**Responsibilities:** -- MessageInput with different modes -- Command suggestions box -- Command suggestion items -- Input validation and handling - -### 5. Sidebar Components (SidebarComponents.kt) -**Responsibilities:** -- SidebarOverlay with navigation -- ChannelsSection for channel management -- PeopleSection for peer list -- Sidebar state management - -### 6. Dialog Components (DialogComponents.kt) -**Responsibilities:** -- Password prompt dialog -- App info dialog -- Other modal dialogs - -### 7. UI Utils (ChatUIUtils.kt) -**Responsibilities:** -- RSSI color mapping -- Text formatting utilities -- Common styling constants -- Helper functions - -## Benefits -- Each file has a single, clear responsibility -- Components are easier to test in isolation -- Better code organization and navigation -- Simplified debugging -- Easier to add new UI features diff --git a/REFACTORING_PROGRESS.md b/REFACTORING_PROGRESS.md deleted file mode 100644 index 147a91d0..00000000 --- a/REFACTORING_PROGRESS.md +++ /dev/null @@ -1,103 +0,0 @@ -# BluetoothMeshService Refactoring - Progress Report - -## ✅ COMPLETED: Extracted Components (All compile successfully) - -### 1. PeerManager.kt (161 lines) -**Responsibilities:** -- Active peer tracking and lifecycle management -- Peer nickname management and stale peer cleanup -- RSSI tracking and peer list updates -- **Interface:** `PeerManagerDelegate` - -### 2. FragmentManager.kt (194 lines) -**Responsibilities:** -- Message fragmentation for large messages (>500 bytes) -- Fragment reassembly and cleanup -- Fragment timeout management (30 seconds) -- **Interface:** `FragmentManagerDelegate` - -### 3. SecurityManager.kt (236 lines) -**Responsibilities:** -- Duplicate detection and replay attack protection -- Key exchange handling and validation -- Message encryption/decryption operations -- Packet signature verification -- **Interface:** `SecurityManagerDelegate` - -### 4. StoreForwardManager.kt (295 lines) -**Responsibilities:** -- Message caching for offline peers (12 hours regular, unlimited favorites) -- Store-and-forward delivery when peers come online -- Cache cleanup and management -- **Interface:** `StoreForwardManagerDelegate` - -### 5. MessageHandler.kt (284 lines) -**Responsibilities:** -- Processing different message types (ANNOUNCE, MESSAGE, LEAVE, etc.) -- Broadcast vs private message handling -- Message relay logic with adaptive probability -- Delivery acknowledgment sending -- **Interface:** `MessageHandlerDelegate` - -### 6. BluetoothConnectionManager.kt (611 lines) -**Responsibilities:** -- BLE advertising and scanning -- GATT server/client setup and management -- Device connection tracking (both server and client modes) -- Packet broadcasting to all connected devices -- **Interface:** `BluetoothConnectionManagerDelegate` - -## 📊 Size Reduction Analysis - -| Component | Lines | Responsibility | -|-----------|--------|----------------| -| **PeerManager** | 161 | Peer lifecycle & tracking | -| **FragmentManager** | 194 | Message fragmentation | -| **SecurityManager** | 236 | Security & encryption | -| **StoreForwardManager** | 295 | Offline message caching | -| **MessageHandler** | 284 | Message type processing | -| **BluetoothConnectionManager** | 611 | BLE connection management | -| **Original File** | ~1000+ | All responsibilities mixed | - -**Total Extracted:** ~1781 lines (distributed across 6 focused files) -**Reduction Factor:** Original single file → 6 smaller, focused components - -## 🏗️ Next Steps for Integration - -### Phase 1: Refactor Existing BluetoothMeshService -1. **Update BluetoothMeshService.kt** to use the new components -2. **Wire up all delegate interfaces** -3. **Maintain exact same public API** so ChatViewModel doesn't change -4. **Test compilation and functionality** - -### Phase 2: Integration Testing -1. **Verify all existing functionality works** -2. **Test key scenarios:** - - Peer discovery and connection - - Message sending/receiving - - Fragment handling - - Store-and-forward delivery - - Security validation - -### Phase 3: Benefits Validation -1. **Easier unit testing** (each component can be tested independently) -2. **Better code maintainability** (clear separation of concerns) -3. **Improved debugging** (isolated component logs) -4. **Future extensibility** (easier to add new features) - -## 🔧 Current Build Status -✅ **All 6 extracted components compile successfully** -✅ **No breaking changes to existing interfaces** -✅ **Original BluetoothMeshService.kt still intact** -✅ **Ready for integration phase** - -## 💡 Key Design Decisions Made - -1. **Delegate Pattern:** Each component uses a delegate interface for clean separation -2. **Coroutine Scope per Component:** Isolated lifecycle management -3. **Thread-Safe Collections:** Maintained from original implementation -4. **Same UUIDs and Constants:** No protocol changes -5. **Preserved iOS Compatibility:** All timing and logic matches iOS exactly -6. **Error Handling:** Maintained original defensive programming patterns - -The refactoring successfully breaks down a monolithic 1000+ line service into 6 focused, maintainable components while preserving 100% compatibility with the iOS implementation. diff --git a/RSSI_COLOR_FIX_SUMMARY.md b/RSSI_COLOR_FIX_SUMMARY.md deleted file mode 100644 index 78c616c5..00000000 --- a/RSSI_COLOR_FIX_SUMMARY.md +++ /dev/null @@ -1,80 +0,0 @@ -# RSSI Color Change Fix - Implementation Summary - -## Problem Identified -The username colors in the chat were not changing based on RSSI signal strength even though RSSI values were being logged as changing. Investigation revealed that: - -1. **RSSI values were only captured once** during the initial BLE scan discovery -2. **No mechanism existed** to continuously update RSSI values from connected devices -3. **UI was not being notified** of RSSI changes to trigger recomposition -4. **Chat rendering was using stale RSSI values** that never changed after initial connection - -## Root Cause -The `BluetoothMeshService` was only recording RSSI during the scan phase (`handleScanResult`) but never updating it during the connection lifetime. Bluetooth GATT provides `readRemoteRssi()` for connected devices, but this wasn't being used. - -## Solution Implemented - -### 1. Device-to-Peer ID Mapping -- Added `deviceToPeerIDMapping` to link Bluetooth devices to peer IDs -- This allows RSSI updates to be associated with the correct peer ID - -### 2. GATT RSSI Reading Callback -- Added `onReadRemoteRssi()` callback in the GATT client callback -- Updates `peerRSSI` map when new RSSI values are read -- Maps device addresses to peer IDs for proper tracking - -### 3. Periodic RSSI Monitoring -- Added background coroutine that runs every 5 seconds -- Calls `readRemoteRssi()` on all active GATT connections -- Provides continuous RSSI updates during connection lifetime - -### 4. UI Notification System -- Added `didUpdateRSSI()` delegate method to notify UI of RSSI changes -- When RSSI changes, the delegate is called to potentially trigger UI updates -- Chat screen automatically recomposes when RSSI values change - -### 5. Key Exchange Enhancement -- Modified `handleKeyExchange()` to map devices to peer IDs -- Transfers any existing peripheral RSSI data to peer-based tracking -- Ensures proper RSSI association after peer identification - -## Code Changes Made - -### BluetoothMeshService.kt -1. **Added device mapping**: `deviceToPeerIDMapping` concurrent hash map -2. **RSSI callback**: `onReadRemoteRssi()` implementation in GATT callback -3. **Periodic monitoring**: `monitorRSSI()` function called every 5 seconds -4. **Key exchange update**: Links devices to peer IDs for RSSI tracking -5. **Delegate method**: `didUpdateRSSI()` interface method for UI notifications - -### ChatViewModel.kt -1. **Delegate implementation**: Added `didUpdateRSSI()` method -2. **Logging**: Debug output when RSSI values change - -### ChatScreen.kt -- **No changes needed** - existing `getRSSIColor(rssi)` function already works -- UI automatically recomposes when `meshService.getPeerRSSI()` returns updated values - -## How It Works Now - -1. **Initial Discovery**: RSSI captured during BLE scan (as before) -2. **Connection**: Device mapped to peer ID during key exchange -3. **Monitoring**: Every 5 seconds, `readRemoteRssi()` called on connected devices -4. **Update**: RSSI callback updates `peerRSSI` map with new values -5. **Notification**: Delegate notified of RSSI change -6. **Rendering**: Chat screen uses updated RSSI values for color calculation -7. **Recomposition**: UI automatically updates with new colors - -## Expected Behavior -- Username colors now change dynamically as users move closer/farther away -- Colors reflect real-time signal strength: green (strong) → yellow (medium) → red (weak) -- Updates occur every 5 seconds while devices are connected -- Your own username remains green regardless of signal strength -- Works for both client and server GATT connections - -## Testing -- Build successful with `./gradlew assembleDebug` -- No compilation errors -- All existing functionality preserved -- Ready for testing with actual devices - -The fix addresses the core issue where RSSI values were static after connection. Now they continuously update, providing the dynamic color feedback based on signal strength that was originally intended. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ce8717bd..be7f1b23 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.bitchat.android" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = 1 - versionName = "1.0" + versionCode = 2 + versionName = "0.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/demo_colors.md b/demo_colors.md deleted file mode 100644 index c463b41c..00000000 --- a/demo_colors.md +++ /dev/null @@ -1,62 +0,0 @@ -# Username Color Feature Demo - -The bitchat Android app already has the username color feature fully implemented! Here's how it works: - -## How It Works - -1. **Your username remains green** - Uses `colorScheme.primary` (bright green in dark mode, dark green in light mode) -2. **Other users get unique colors** - Based on their peer ID using the `getUsernameColor()` function -3. **Colors are consistent** - Same user always gets the same color across sessions -4. **Terminal-friendly palette** - 16 colors that work on both black and white backgrounds - -## Color Palette - -The system uses these 16 terminal-friendly colors for other users: - -- 🟢 Bright Green (#00FF00) -- 🔵 Cyan (#00FFFF) -- 🟡 Yellow (#FFFF00) -- 🔴 Magenta (#FF00FF) -- 🟦 Bright Blue (#0080FF) -- 🟠 Orange (#FF8000) -- 🔶 Lime Green (#80FF00) -- 🟣 Purple (#8000FF) -- 🩷 Pink (#FF0080) -- 💚 Spring Green (#00FF80) -- 🟦 Light Cyan (#80FFFF) -- 🩷 Light Red (#FF8080) -- 🟦 Light Blue (#8080FF) -- 🟡 Light Yellow (#FFFF80) -- 🩷 Light Magenta (#FF80FF) -- 🟢 Light Green (#80FF80) - -## Example Chat Display - -``` -[14:23:45] <@you> hello everyone! ← Your message (green) -[14:23:47] <@alice> hey there! ← Alice (cyan) -[14:23:50] <@bob> how's it going? ← Bob (yellow) -[14:23:52] <@charlie> great to see you all ← Charlie (magenta) -[14:23:55] <@you> having a great time ← Your message (green) -[14:23:58] <@alice> same here @you! ← Alice (cyan again) -``` - -## Code Implementation - -The feature is implemented in `/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt`: - -- Line 342-350: Color assignment logic in `formatMessageAsAnnotatedString()` -- Line 820-855: `getUsernameColor()` function that generates consistent colors -- Uses peer ID for consistency (falls back to nickname if no peer ID available) -- Integrates perfectly with the existing IRC-style chat format - -## Testing - -The feature is already working in the app. When you chat with multiple users, you'll see: -- Your messages in green -- Each other user in their own unique color -- Same user always has the same color -- Colors remain consistent across app restarts -- Works in both light and dark themes - -The feature is **complete and ready to use**! 🎉 diff --git a/refactoring_plan.md b/refactoring_plan.md deleted file mode 100644 index fe61e413..00000000 --- a/refactoring_plan.md +++ /dev/null @@ -1,73 +0,0 @@ -# BluetoothMeshService Refactoring Plan - -## Current State -- Single file: `BluetoothMeshService.kt` (~1000+ lines) -- Multiple responsibilities mixed together -- Hard to test and maintain - -## Proposed Structure - -### 1. Core Service (BluetoothMeshService.kt) -**Responsibilities:** -- Service lifecycle management -- Coordination between components -- Public API for sending messages -- Delegate management - -### 2. Connection Management (BluetoothConnectionManager.kt) -**Responsibilities:** -- BLE scanning and advertising -- GATT server/client setup and management -- Device connection tracking -- Peer discovery and RSSI tracking - -### 3. Packet Processing (PacketProcessor.kt) -**Responsibilities:** -- Incoming packet handling -- Message type routing -- TTL and duplicate detection -- Timestamp validation - -### 4. Message Handler (MessageHandler.kt) -**Responsibilities:** -- Processing specific message types (ANNOUNCE, MESSAGE, LEAVE, etc.) -- Message parsing and validation -- Relay logic - -### 5. Fragment Manager (FragmentManager.kt) -**Responsibilities:** -- Message fragmentation for large messages -- Fragment reassembly -- Fragment cleanup and timeouts - -### 6. Store-and-Forward Manager (StoreForwardManager.kt) -**Responsibilities:** -- Message caching for offline peers -- Delivering cached messages when peers come online -- Cache cleanup and management - -### 7. Peer Manager (PeerManager.kt) -**Responsibilities:** -- Active peer tracking -- Peer nickname management -- Stale peer cleanup -- Peer list updates - -### 8. Security Manager (SecurityManager.kt) -**Responsibilities:** -- Key exchange handling -- Message signing and verification -- Duplicate detection tracking - -## Refactoring Strategy -1. Extract each component while maintaining exact functionality -2. Use dependency injection for component communication -3. Ensure all existing tests pass -4. Maintain the same public API - -## Benefits -- Easier to test individual components -- Better separation of concerns -- More maintainable code -- Easier to add new features -- Better code reuse From d490393de6590cf2aab9371a44f97c4c0ccc2891 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:32:08 +0200 Subject: [PATCH 30/41] back gesture works --- .../java/com/bitchat/android/MainActivity.kt | 20 +++++++ .../java/com/bitchat/android/ui/ChatScreen.kt | 12 ++-- .../java/com/bitchat/android/ui/ChatState.kt | 9 +++ .../com/bitchat/android/ui/ChatViewModel.kt | 56 +++++++++++++++++++ 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index e07873e8..2323c7fd 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.lifecycleScope import androidx.lifecycle.ViewModelProvider +import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.onboarding.* import com.bitchat.android.ui.ChatScreen @@ -138,6 +140,24 @@ class MainActivity : ComponentActivity() { } OnboardingState.COMPLETE -> { + // Set up back navigation handling for the chat screen + val backCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + // Let ChatViewModel handle navigation state + val handled = chatViewModel.handleBackPressed() + if (!handled) { + // If ChatViewModel doesn't handle it, disable this callback + // and let the system handle it (which will exit the app) + this.isEnabled = false + onBackPressedDispatcher.onBackPressed() + this.isEnabled = true + } + } + } + + // Add the callback - this will be automatically removed when the activity is destroyed + onBackPressedDispatcher.addCallback(this, backCallback) + ChatScreen(viewModel = chatViewModel) } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index 4fe40704..4c9e30dc 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -60,15 +60,15 @@ fun ChatScreen(viewModel: ChatViewModel) { val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet()) val privateChats by viewModel.privateChats.observeAsState(emptyMap()) val channelMessages by viewModel.channelMessages.observeAsState(emptyMap()) - var showSidebar by remember { mutableStateOf(false) } + val showSidebar by viewModel.showSidebar.observeAsState(false) val showCommandSuggestions by viewModel.showCommandSuggestions.observeAsState(false) val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList()) + val showAppInfo by viewModel.showAppInfo.observeAsState(false) var messageText by remember { mutableStateOf("") } var showPasswordPrompt by remember { mutableStateOf(false) } var showPasswordDialog by remember { mutableStateOf(false) } var passwordInput by remember { mutableStateOf("") } - var showAppInfo by remember { mutableStateOf(false) } // Show password dialog when needed LaunchedEffect(showPasswordPrompt) { @@ -142,8 +142,8 @@ fun ChatScreen(viewModel: ChatViewModel) { nickname = nickname, viewModel = viewModel, colorScheme = colorScheme, - onSidebarToggle = { showSidebar = true }, - onShowAppInfo = { showAppInfo = true }, + onSidebarToggle = { viewModel.showSidebar() }, + onShowAppInfo = { viewModel.showAppInfo() }, onPanicClear = { viewModel.panicClearAllData() } ) @@ -162,7 +162,7 @@ fun ChatScreen(viewModel: ChatViewModel) { ) { SidebarOverlay( viewModel = viewModel, - onDismiss = { showSidebar = false }, + onDismiss = { viewModel.hideSidebar() }, modifier = Modifier.fillMaxSize() ) } @@ -188,7 +188,7 @@ fun ChatScreen(viewModel: ChatViewModel) { passwordInput = "" }, showAppInfo = showAppInfo, - onAppInfoDismiss = { showAppInfo = false } + onAppInfoDismiss = { viewModel.hideAppInfo() } ) } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatState.kt b/app/src/main/java/com/bitchat/android/ui/ChatState.kt index 748062b1..4a2e9bda 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatState.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatState.kt @@ -85,6 +85,10 @@ class ChatState { val peerIDToPublicKeyFingerprint = mutableMapOf() + // Navigation state + private val _showAppInfo = MutableLiveData(false) + val showAppInfo: LiveData = _showAppInfo + // Unread state computed properties val hasUnreadChannels: MediatorLiveData = MediatorLiveData() val hasUnreadPrivateMessages: MediatorLiveData = MediatorLiveData() @@ -118,6 +122,7 @@ class ChatState { fun getShowCommandSuggestionsValue() = _showCommandSuggestions.value ?: false fun getCommandSuggestionsValue() = _commandSuggestions.value ?: emptyList() fun getFavoritePeersValue() = _favoritePeers.value ?: emptySet() + fun getShowAppInfoValue() = _showAppInfo.value ?: false // Setters for state updates fun setMessages(messages: List) { @@ -201,5 +206,9 @@ class ChatState { Log.d("ChatState", "LiveData value after set: ${_favoritePeers.value}") Log.d("ChatState", "LiveData has active observers: ${_favoritePeers.hasActiveObservers()}") } + + fun setShowAppInfo(show: Boolean) { + _showAppInfo.value = show + } } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index e8ad9c8d..190f696d 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -71,6 +71,7 @@ class ChatViewModel( val showCommandSuggestions: LiveData = state.showCommandSuggestions val commandSuggestions: LiveData> = state.commandSuggestions val favoritePeers: LiveData> = state.favoritePeers + val showAppInfo: LiveData = state.showAppInfo init { // Note: Mesh service delegate is now set by MainActivity @@ -366,4 +367,59 @@ class ChatViewModel( // Note: Mesh service restart is now handled by MainActivity // This method now only clears data, not mesh service lifecycle } + + // MARK: - Navigation Management + + fun showAppInfo() { + state.setShowAppInfo(true) + } + + fun hideAppInfo() { + state.setShowAppInfo(false) + } + + fun showSidebar() { + state.setShowSidebar(true) + } + + fun hideSidebar() { + state.setShowSidebar(false) + } + + /** + * Handle Android back navigation + * Returns true if the back press was handled, false if it should be passed to the system + */ + fun handleBackPressed(): Boolean { + return when { + // Close app info dialog + state.getShowAppInfoValue() -> { + hideAppInfo() + true + } + // Close sidebar + state.getShowSidebarValue() -> { + hideSidebar() + true + } + // Close password dialog + state.getShowPasswordPromptValue() -> { + state.setShowPasswordPrompt(false) + state.setPasswordPromptChannel(null) + true + } + // Exit private chat + state.getSelectedPrivateChatPeerValue() != null -> { + endPrivateChat() + true + } + // Exit channel view + state.getCurrentChannelValue() != null -> { + switchToChannel(null) + true + } + // No special navigation state - let system handle (usually exits app) + else -> false + } + } } From bc344879e71486e968fe8c83126d0fbd25a63710 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:33:11 +0200 Subject: [PATCH 31/41] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a5ca76b..8accb389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Monochrome/themed launcher icon for Android 12+ dynamic theming support - Unit tests package with initial testing infrastructure - Production build optimization with code minification and shrinking +- Native back gesture/button handling for all app views ### Fixed - Favorite peer functionality completely restored and improved From ad6dc3680b9a7a6469f38a110481707c1acbe77f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:40:25 +0200 Subject: [PATCH 32/41] permissions layout --- CHANGELOG.md | 5 + .../java/com/bitchat/android/MainActivity.kt | 3 - .../onboarding/PermissionExplanationScreen.kt | 213 +++++++++--------- .../android/onboarding/PermissionManager.kt | 8 +- 4 files changed, 112 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8accb389..6e8bd3aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed +- Permission onboarding screen UX: removed "Exit App" button and fixed "Grant Permissions" button positioning to always be visible + ## [0.6] ### Added diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 2323c7fd..5997a9f1 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -124,9 +124,6 @@ class MainActivity : ComponentActivity() { onContinue = { onboardingState = OnboardingState.PERMISSION_REQUESTING onboardingCoordinator.requestPermissions() - }, - onCancel = { - finish() } ) } diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt index f09d9783..b352dc1f 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt @@ -21,120 +21,130 @@ import androidx.compose.ui.unit.sp @Composable fun PermissionExplanationScreen( permissionCategories: List, - onContinue: () -> Unit, - onCancel: () -> Unit + onContinue: () -> Unit ) { val colorScheme = MaterialTheme.colorScheme val scrollState = rememberScrollState() - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp) - .verticalScroll(scrollState), - verticalArrangement = Arrangement.spacedBy(16.dp) + Box( + modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.height(24.dp)) - // Header + // Scrollable content Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Welcome to bitchat*", - style = MaterialTheme.typography.headlineMedium.copy( - fontFamily = FontFamily.Monospace, - fontWeight = FontWeight.Bold, - color = colorScheme.primary - ), - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "Decentralized mesh messaging over Bluetooth", - style = MaterialTheme.typography.bodyMedium.copy( - fontFamily = FontFamily.Monospace, - color = colorScheme.onSurface.copy(alpha = 0.7f) - ), - textAlign = TextAlign.Center - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Privacy assurance section - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = colorScheme.surfaceVariant.copy(alpha = 0.3f) - ), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + .padding(bottom = 88.dp) // Leave space for the fixed button + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { + Spacer(modifier = Modifier.height(24.dp)) + // Header Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + Text( + text = "Welcome to bitchat*", + style = MaterialTheme.typography.headlineMedium.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + color = colorScheme.primary + ), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Decentralized mesh messaging over Bluetooth", + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace, + color = colorScheme.onSurface.copy(alpha = 0.7f) + ), + textAlign = TextAlign.Center + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Privacy assurance section + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "🔒", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.size(20.dp) + ) + Text( + text = "Your Privacy is Protected", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Bold, + color = colorScheme.onSurface + ) + ) + } + Text( - text = "🔒", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.size(20.dp) - ) - Text( - text = "Your Privacy is Protected", - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.Bold, - color = colorScheme.onSurface + text = "• bitchat doesn't track you or collect personal data\n" + + "• No servers, no internet required, no data logging\n" + + "• Location permission is only used by Android for Bluetooth scanning\n" + + "• Your messages stay on your device and peer devices only", + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + color = colorScheme.onSurface.copy(alpha = 0.8f) ) ) } - - Text( - text = "• bitchat doesn't track you or collect personal data\n" + - "• No servers, no internet required, no data logging\n" + - "• Location permission is only used by Android for Bluetooth scanning\n" + - "• Your messages stay on your device and peer devices only", - style = MaterialTheme.typography.bodySmall.copy( - fontFamily = FontFamily.Monospace, - color = colorScheme.onSurface.copy(alpha = 0.8f) - ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "To work properly, bitchat needs these permissions:", + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ) + ) + + // Permission categories + permissionCategories.forEach { category -> + PermissionCategoryCard( + category = category, + colorScheme = colorScheme ) } + + Spacer(modifier = Modifier.height(24.dp)) } - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "To work properly, bitchat needs these permissions:", - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium, - color = colorScheme.onSurface - ) - ) - - // Permission categories - permissionCategories.forEach { category -> - PermissionCategoryCard( - category = category, - colorScheme = colorScheme - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Action buttons - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp) + // Fixed button at bottom + Surface( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth(), + color = colorScheme.surface, + shadowElevation = 8.dp ) { Button( onClick = onContinue, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), colors = ButtonDefaults.buttonColors( containerColor = colorScheme.primary ) @@ -148,24 +158,7 @@ fun PermissionExplanationScreen( modifier = Modifier.padding(vertical = 4.dp) ) } - - OutlinedButton( - onClick = onCancel, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = colorScheme.onSurface.copy(alpha = 0.7f) - ) - ) { - Text( - text = "Exit App", - style = MaterialTheme.typography.bodyMedium.copy( - fontFamily = FontFamily.Monospace - ), - modifier = Modifier.padding(vertical = 4.dp) - ) - } } - Spacer(modifier = Modifier.height(24.dp)) } } diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt index 7060fb1c..38ea0d6f 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt @@ -116,7 +116,7 @@ class PermissionManager(private val context: Context) { categories.add( PermissionCategory( name = "Nearby Devices", - description = "Required to discover and connect to other bitchat users via Bluetooth", + description = "Required to discover bitchat users via Bluetooth", permissions = bluetoothPermissions, isGranted = bluetoothPermissions.all { isPermissionGranted(it) }, systemDescription = "Allow bitchat to connect to nearby devices" @@ -132,10 +132,10 @@ class PermissionManager(private val context: Context) { categories.add( PermissionCategory( name = "Precise Location", - description = "Required by Android for Bluetooth scanning.", + description = "Required by Android to discover nearby bitchat users via Bluetooth", permissions = locationPermissions, isGranted = locationPermissions.all { isPermissionGranted(it) }, - systemDescription = "Allow bitchat to access this device's location" + systemDescription = "bitchat needs this to scan for nearby devices" ) ) @@ -144,7 +144,7 @@ class PermissionManager(private val context: Context) { categories.add( PermissionCategory( name = "Notifications", - description = "Show notifications when you receive private messages while the app is in background", + description = "Notifications to keep you updated", permissions = listOf(Manifest.permission.POST_NOTIFICATIONS), isGranted = isPermissionGranted(Manifest.permission.POST_NOTIFICATIONS), systemDescription = "Allow bitchat to send you notifications" From ee70073324395b10046c0c9be7d2c2b0f2993b9a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:43:13 +0200 Subject: [PATCH 33/41] notifications description --- .../java/com/bitchat/android/onboarding/PermissionManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt index 5db01a2d..7d293324 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt @@ -144,7 +144,7 @@ class PermissionManager(private val context: Context) { categories.add( PermissionCategory( type = PermissionType.NOTIFICATIONS, - description = "Notifications to keep you updated", + description = "Receive notifications when you receive private messages", permissions = listOf(Manifest.permission.POST_NOTIFICATIONS), isGranted = isPermissionGranted(Manifest.permission.POST_NOTIFICATIONS), systemDescription = "Allow bitchat to send you notifications" From ce93900ef6c45c6ed9721c404caf57ae55391b6f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 18:43:35 +0200 Subject: [PATCH 34/41] icons --- .../res/drawable/ic_launcher_background.xml | 10 +-- .../res/drawable/ic_launcher_foreground.xml | 60 +++------------ .../res/drawable/ic_launcher_monochrome.xml | 70 ++++-------------- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 575 -> 2265 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 575 -> 0 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 661 -> 1295 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 661 -> 0 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 726 -> 3009 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 726 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1115 -> 4975 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 1115 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 2162 -> 7449 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 2162 -> 0 bytes 13 files changed, 30 insertions(+), 110 deletions(-) delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index faef2e6f..b63113ef 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,10 +1,10 @@ + android:height="108dp"> + android:pathData="M-0.2617076 -0.1145289H135.7284V135.5812H-0.2617076V-0.1145289Z" + android:fillColor="#000000" /> diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 91e2961e..dbc4ad64 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,51 +1,15 @@ - - - - - - - - - - - - - - - - - - - - - - + android:height="108dp"> + + + diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml index c531b643..a9e8c8af 100644 --- a/app/src/main/res/drawable/ic_launcher_monochrome.xml +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -1,60 +1,16 @@ - - - - - - - - - - + android:height="108dp"> + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index 096a00946c109c45ddd88b93da5ecdacf6fa2383..d12842543e4f9506daa47b4b7b38459fb7e9e157 100644 GIT binary patch literal 2265 zcmV;~2qyQ5P)Px-lSxEDRCr$PoNH_pRTRh1w%tNo+Dh8S4}LM>1EGrzNlke5{g9Rd1&q-IW1=DP z1Jt0=M1qAxHz_84&=;lpL6SzJ5y8|31q63%ACw5TE;W2Ap-F)dYK)e)l(uWmxObfn zbLXBrcea4pO*ZZBeat<-`#ZwD`}gmMhZ79`hr#au`gYos zhWe!Rr{`kU&a6MI7at!F$;rtdkBp2Q0)Pj6oB&pU5RN-5E9;~2@$vU&W@i4?BmoMq z9;L0_In5q*b}EZ@Y5#=nnueYvyh|K&YN2FM9{Q7aB|7u>vAfCNs-2bt%f`_2=FFlFXu zR)CgmJJ9d#Yu~!p4j{^`I#q`o000xV0T~A&6L%;^M!Jrb!aT)QErD~5HUaJL_x66Wh3sNfLn#VV zQHD(ao}xW1fgkNE$N=a`SCZKTs=3(yes52E#R3J1VveZXDwfFxTb@#6`cYPp8KD1@ zk{nZz85B{lb}}YW`x3=2WBID0JuSm3F^DOMq8ckBL@;ICLS~UFI{8&n&=w5R$2MDL zF;UDBl~dPOB4rqlL#?l*ASxiLks?@BH=@>~s8T6NBcPtPa&i`Pxm?O;{Zu7T>xq<< zS-jVN43YsPiZQa~l-YMeNsa-+0aFx0aU;q~SJaP48Pq`T2q0dHH&cbIQe-k8PgN>P zQUmmUzxU_1GFw?pqyTCRak`#yvL7iRmFz}6B~eiiwp9XDfeJdh$ux^m6cQU73keAc zT2(Yye{EY2VCmAO5EmC0u7_n46L@(Bfc3!xHZ?T`v$M0@>?xY0hykLUdK45Cz=;zl z;QICJnz4zAiO}8M4b|1vaOch)#u!>#TVdV0b+BT^3K$(7h4Jxm$j{H$$~SD-05vr= z+W!c0_3G6yJUk4OlXw9TVBfxdaQX7(FkEVIW|QVAsj{EuQ9<3UFYGnTUOWZqzehl| zwY5-KSg09^n<)c|2XO-)U(a^*@W zFE7`OW3Tk|bm;HzhaEe1Kp+q>RRjV;hzNdga4_7K8n4WsN9tLED&j6wOF$Kx4hm7?j0%(C{ zx(~*KBcM~KPQmTlx54dpLrO{tw6(Rt!Gi}MatlI2DdnDv@mALC zdj!N>3~ya$XJ>1leLkNC3A2yh#q@y8j731by}eoh*Si=c*6?Dc5}v1(msn;ot8AuM5Cc?N zS_Q&QUd6+3kw9aAWyhP&&Af8f0sh$DCzbwsn zc6MqhyrQB4*e^yfjbobc>+9397y{C}A2W^t%FD|W25FI!*w`Hd#5qRr zc@joOMzr-zYHBKUb#=kv!-rvPY%I(j1NZv%>$QdAjT<+#g(AA3F?-@75n~Vn;>952 zq=}X&jsfDWcZ^2pKjY~Gu72?~KR+LiL&m;v0f~!6y>WP|VKfdQY((eAD1#9Ut^K*Y@WKZX2w#k zOo|MZE$2<95}-|ZvMK;1icJ*Nh*UyxHxaN!10>1{l*=Gifn8*%Y`JKO;+TR&U=~YB zj~yWYkwU4ndDSdNaVM%viqwPU$_g?95-CL%5U)R3rBD=Z222@{uc5%vEG7$378FG_ zj-`3bViZ7DgOUl_l#C|=w&;L*%&$zO3qCGi;T$^dLmp&1}&6>?ktI$@1q)s%-7mz#WH)IjAWVDxk+r n4U0COC?EmqD4-}HHE;X}gcJDRqM+P$00000NkvXXu0mjf)9M|} literal 575 zcmeAS@N?(olHy`uVBq!ia0vp^J|N5iBp4q3;rkAx6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!u;#M9T6{Utk-fRgol%^$2lA*Lj6cNd2L?fqx= zGcYhJd%8G=L>zv5b)&Xxpv-}f_Y*#RnsP5itoxH#ebdR=JR+$RRGJ_5EuS*aC?--z zVxG%P9kIvWm%2kwDa}6WogZZM;Psqp)h~yN>nGREe>%r+`}=}-w$HmHl6wrF?T=Lc zSWs*+**kpKj|I^wE&9vqx>&Efw%%wgUbm;3{i{<%X8+#g6|IK)m*mw~6-+zFw!izV z>g0395moVJ0ssCauzb}2rqk=m>3210Z{77PNrwZjc8TgG8dc}mcPVYZ+faH^uDaMr zrOI%f*P_koJNNyJo_GApy|nYTy?kqDge1i1nsKkr+8B5vu-o_Ww5S|@u`Nl${sCuX zxTimUbgcD%j`qo?JKo6t6c#-8C*J?S>9pw0*}K2qNZz~6=)#}&OHOA%zHqy)oqx08 z+SEdu6ky<~mbgZgq$HN4S|t~y0x1R~10z#i19M#iqYy(gD`OKYQ%h|F11kfAUjjX~ yC>nC}Q!>*kack)IH8}{>paHj`Br`X)xFj*R0Joky5u$QHJq(_%elF{r5}E+^I^4nl diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 096a00946c109c45ddd88b93da5ecdacf6fa2383..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 575 zcmeAS@N?(olHy`uVBq!ia0vp^J|N5iBp4q3;rkAx6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!u;#M9T6{Utk-fRgol%^$2lA*Lj6cNd2L?fqx= zGcYhJd%8G=L>zv5b)&Xxpv-}f_Y*#RnsP5itoxH#ebdR=JR+$RRGJ_5EuS*aC?--z zVxG%P9kIvWm%2kwDa}6WogZZM;Psqp)h~yN>nGREe>%r+`}=}-w$HmHl6wrF?T=Lc zSWs*+**kpKj|I^wE&9vqx>&Efw%%wgUbm;3{i{<%X8+#g6|IK)m*mw~6-+zFw!izV z>g0395moVJ0ssCauzb}2rqk=m>3210Z{77PNrwZjc8TgG8dc}mcPVYZ+faH^uDaMr zrOI%f*P_koJNNyJo_GApy|nYTy?kqDge1i1nsKkr+8B5vu-o_Ww5S|@u`Nl${sCuX zxTimUbgcD%j`qo?JKo6t6c#-8C*J?S>9pw0*}K2qNZz~6=)#}&OHOA%zHqy)oqx08 z+SEdu6ky<~mbgZgq$HN4S|t~y0x1R~10z#i19M#iqYy(gD`OKYQ%h|F11kfAUjjX~ yC>nC}Q!>*kack)IH8}{>paHj`Br`X)xFj*R0Joky5u$QHJq(_%elF{r5}E+^I^4nl diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index eb98d48bdff23efdbd4c1135ad622121c87600b4..c7771a9e4f9d2f709b4fa24d834710da3898f4b1 100644 GIT binary patch literal 1295 zcmV+q1@QWbP)Px($w@>(RA@u(noCG4XB5Z(sgF1@(M%>(R8(-Ii)8E4mAG@GZd7z36zaaxMK4P6 z7QCPhy=Itu9jWoXcb|sC*m;sRCCE;-R=jZ3=AE`q!`AbC%H0lbP7AOIy83|{LyJ29B+eeQF5r>Lr01>nXOKv5J8zzD$| z^lLqrGl1DFa57!Af1EdBjR8BYo&>L%Ka5moOdfl*I@RC@T zW!n(4gK}!1ofGJN$z+lT;P?Ak0Ct1gttFiPSt-B;$X8BqLD&jZR#v7JFk27I47XeO z@)T0M5YhvPl35fU+2#gQ1s*^}MTMmTsljAfMt^@lR8>VJ62Z>S4sLI6QB+if&dyHM z)YRbh^%W~CE4aG4LQhW*s;bC_&uwvW5jQtC++DA80HXqWPc=0)VQOj$6B83?Z*Rxt zk3Z(a_L<+1Xhwru7PJZEdCdrvTR0)-n{Zo5kM71j=cwzf7p z@@WZue}AtfaaUItj*pMAu&|)z1vPF6nT2jv09H8=2m~-NFn~lNfz#7dY;SKP8jWhx zXm@uvs;jHDd&|qq+B8Z{k|cyeA)K9^;rjZspG32dL4BSA9*>8W1NxUcS)DnHmN9G{ z<$w`Dlmlkl3{f(2S7?*C*ALHAK$0X@4u}*G8EXe&V?5n<1|Ucz(A0^llO75Ach4n6M-!0mSDGz*!OD1fwKW;HU2w(YVke|UO&`p(H>md8-Z;FTol zw`er_5x}1So~%kt)R9ym2&zZGN7 zWTP4})x_wc&l%0`Uk&W;{~KUm6u0$*{ud0)$qDZs;BN(tNsD2;iShsd002ovPDHLk FV1h++RRaJ3 literal 661 zcmeAS@N?(olHy`uVBq!ia0vp^6(Gz3Bp77)Gx>m&Lb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=Ijcz0ZOo!c>21szhq|;P_lln`GXZG#FXUi?!xfDz5mR9 z1_s7TPZ!6KjC*fy*lV;D${hcAU$j)%bEAaDVbzVnUY=fFlK#sk&lVA0on&}pild~j zS0`ul(W#C`c6uj2#Jrp1G-dVnhXvxXKY#z0vRnW5<sHK+L9B(J`GGJ00s(yfWcuXO(iv?WTU$;aj0?fk*De!q+fUoL~e zzkjQ&AMCiK?4nJ#?9&@c%;(t_4TyE94R` ze-F9N`<^*vR!wVuvjRuc=a|Bmmc5qo4~=%Ny_l6QxbMEVi|-+aUAKSAu3Po%+!4J4 z$@33J*-l_zC(RSK&aol2d4o5jd0_Jg9f$RfJLD5iDcoV)kRp)B7GcVcbBJ%V zp8LE&vA%GJ(ZwG-?V0TPx?zFR@yGmp{jJMtl}cA?fl;Db;u=wsl30>zm0Xkxq!^40 zj7)V6%ykWnLJZBUj7_XeEwv2{tPBi(3G|?9$jwj5OsmALq1)HwAkYR4xD6$lxv9k^ ZiMa*1_1uXNl>_Qw@O1TaS?83{1OT>l{^0-s diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index eb98d48bdff23efdbd4c1135ad622121c87600b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 661 zcmeAS@N?(olHy`uVBq!ia0vp^6(Gz3Bp77)Gx>m&Lb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=Ijcz0ZOo!c>21szhq|;P_lln`GXZG#FXUi?!xfDz5mR9 z1_s7TPZ!6KjC*fy*lV;D${hcAU$j)%bEAaDVbzVnUY=fFlK#sk&lVA0on&}pild~j zS0`ul(W#C`c6uj2#Jrp1G-dVnhXvxXKY#z0vRnW5<sHK+L9B(J`GGJ00s(yfWcuXO(iv?WTU$;aj0?fk*De!q+fUoL~e zzkjQ&AMCiK?4nJ#?9&@c%;(t_4TyE94R` ze-F9N`<^*vR!wVuvjRuc=a|Bmmc5qo4~=%Ny_l6QxbMEVi|-+aUAKSAu3Po%+!4J4 z$@33J*-l_zC(RSK&aol2d4o5jd0_Jg9f$RfJLD5iDcoV)kRp)B7GcVcbBJ%V zp8LE&vA%GJ(ZwG-?V0TPx?zFR@yGmp{jJMtl}cA?fl;Db;u=wsl30>zm0Xkxq!^40 zj7)V6%ykWnLJZBUj7_XeEwv2{tPBi(3G|?9$jwj5OsmALq1)HwAkYR4xD6$lxv9k^ ZiMa*1_1uXNl>_Qw@O1TaS?83{1OT>l{^0-s diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index fa1711674f51554e85eff38e6f8d4deaebaf17cb..c8d2b212df7033c9a9e78eb99ef86f493ce2a50e 100644 GIT binary patch literal 3009 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 D4YGYM literal 726 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 literal 1115 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5890C>L#5>RT|i19*(1o8fuTx`fuW&=f#DZW zsNn?zL#Y7+!>a@a2CEqi4B`cIb_Lo1CD=&O7eDhVff$P ze`Y@e1GBxSi(^Q|t+#i)JwgH{4t!kv*djSfZ0ZBG6RjG8${}pa0z@=ZTyHdUoZ8gt zsM*@;84xh>OT&eZF0NBzO|BP{g#~XcTO_1gDA=N_r6XF^@o4qrzMM%(+gjShdlGx6NGlFU8DlV?HL_gS6hfN;=#%Q#Ma! zyOHT8mCAO)OgTPPQ*;QRv&g45qtAINK*&nEmvlke^WVR40xOYYkJUuWs%$MAkjWTte#?VIvB{I8-G z#>gq`IPX361%qj)S3$$MRc8e9&-OCL%{kG{|MS`%i3RI78XZ@*Jm4?ScjPqRg3{Y& zz|h!dc=Rpfx8)HlGrO<7vHHB@cI1M*WecW0$loYEr@FaR={uXa&DDkZ+&eziIE1w7 zZU}v*xp8ya5>>wxhWpyao5NEWjw@&PiAXx^HFKV6#4KattPb?0f%Ewkwhyz`HpcBg z!LaJ2&)KPx5Bg?CX&WDKH4arVK2T^}>XO!QIBk_v8pCb@3w`4QnZ~7_X$`B>Rt2Ro z{8pV4Phq{7xwm%U$2<b7bsy^6oPE11K zLfZWL&F(P`GdAfRe|DivRl7m`amec8>J4_M&N7~o-LJ4Q<>#tDwa>Hm^WIf>=P6yb zW47KLncLGIwzrqQeDhywflDXfg}srTd>@P!sIdPC5n0Q^o}nT0)x7nN{DRMcksIQmqBidp)5ml`w3vmRxNRjC`m~y zNwrEYN(E93Mg~Tvx(4RD21X%&@~R;HHP1_o9J2EPP)YEd-g=BH$)RpQpr?Q3!n js6hj6LrG?CYH>+oZUJsRcOpdPK>5wn)z4*}Q$iB}=J=@T diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 7c81e83c76f9a47ea4c6ce3985357f92e210b2b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1115 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5890C>L#5>RT|i19*(1o8fuTx`fuW&=f#DZW zsNn?zL#Y7+!>a@a2CEqi4B`cIb_Lo1CD=&O7eDhVff$P ze`Y@e1GBxSi(^Q|t+#i)JwgH{4t!kv*djSfZ0ZBG6RjG8${}pa0z@=ZTyHdUoZ8gt zsM*@;84xh>OT&eZF0NBzO|BP{g#~XcTO_1gDA=N_r6XF^@o4qrzMM%(+gjShdlGx6NGlFU8DlV?HL_gS6hfN;=#%Q#Ma! zyOHT8mCAO)OgTPPQ*;QRv&g45qtAINK*&nEmvlke^WVR40xOYYkJUuWs%$MAkjWTte#?VIvB{I8-G z#>gq`IPX361%qj)S3$$MRc8e9&-OCL%{kG{|MS`%i3RI78XZ@*Jm4?ScjPqRg3{Y& zz|h!dc=Rpfx8)HlGrO<7vHHB@cI1M*WecW0$loYEr@FaR={uXa&DDkZ+&eziIE1w7 zZU}v*xp8ya5>>wxhWpyao5NEWjw@&PiAXx^HFKV6#4KattPb?0f%Ewkwhyz`HpcBg z!LaJ2&)KPx5Bg?CX&WDKH4arVK2T^}>XO!QIBk_v8pCb@3w`4QnZ~7_X$`B>Rt2Ro z{8pV4Phq{7xwm%U$2<b7bsy^6oPE11K zLfZWL&F(P`GdAfRe|DivRl7m`amec8>J4_M&N7~o-LJ4Q<>#tDwa>Hm^WIf>=P6yb zW47KLncLGIwzrqQeDhywflDXfg}srTd>@P!sIdPC5n0Q^o}nT0)x7nN{DRMcksIQmqBidp)5ml`w3vmRxNRjC`m~y zNwrEYN(E93Mg~Tvx(4RD21X%&@~R;HHP1_o9J2EPP)YEd-g=BH$)RpQpr?Q3!n js6hj6LrG?CYH>+oZUJsRcOpdPK>5wn)z4*}Q$iB}=J=@T diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 335769ff8b0ad99eb694df82ff65e208cb7d21df..10fa135ec530ee1d5ad76611cc6087fc6778da90 100644 GIT binary patch literal 7449 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 literal 2162 zcmcJQdpK148pnTYnPC{$#MlUl4xv5Ub&ShQ2VvVNNyzo6498_;928m7=D6F{u6@+D zCs7BvRW2!pbjsN&GkkDUWx1BNgG00~q8gdqUXe*%EQ zzS?ro7JiW8xVk$@%}Ak&x;6`jYeX1A|KjHC2*g>1-8}f+$JKG4chczGcv{|Y!jASO ztKq;z-Ebk%*N1H|l6j-tglXjVtEr?~^z*3vnX@N;s2J?JQ*kBV(4Qm%cdAFuejCmh z$~k|jH!nG7ILfl(pYdCmf4Nc|yJR){B}25dq4i|x$8}MgRv~Y_ZSt4RGbJf$DQ&;> zY#zdw_(wd)@|@Xa}7 zOx(;})x!o5Z-smcQwS<+Ww_TQnt6|T(KCWs`gApAZ(Bs_!j_Ww!+%!u{uXiz95__g|5Wq%6K!T6x)$l~K<~=9H)*nlIwCPuHD5 znq;`^5;Q?;s|7lecpjJ%4*1thP$+f#fgx%zeU+DqX#Qk?X(AVA(#YiIZ-Fan5dU!D zTq@eNjX5J7j85ob#>icfy$shG_@M^c*=VyTh(KAXrfSoSXgq7(Yk3>h8OCTvO!fxJ=-&&7bE?b|V6QIwF&23v&3`R;R2PT3D7SBx zE~HNpi*{q=Qcyr)7K4ZJHNl3MITD~HYbJ0ADFHr&&d+|Z#+oa&`vg)Z%}wy6cJF~s zNX+tAi`$^Xzuyz< zz;4@ZbK;H&v`eYI>dc&w-;I1HEfycS@1e-d0rty1Ny(U#6Ocw}AKTQ)=TfedxHA&0 zqjmR&WcH=*Kv^F^%h#_K1D>grZg}oRS&xA~z2FLMYK-{QCwA2C#h�Hz|lY2ROf7 zn%Bi1AyM>tl1?m;IFfIKc4@SakFSi=5pyrl$tXFrP_4|IA}lV?nF)CbC@T+SZpPT~ zrprfQ1``wptZvm@j(kt(CDnq{+P`o#Mm&rJJ;-bQ0_NNys zHqI`5Gn6$Cg3RaQTP?`Gy4YIO_=Zcv90uueizAdbTdTd7#Op^;hxS9O(~`azODOsi z_Oin_4KLD?QNN`RF?~R%8VcbH^7JM6@r3$tY^XdTT=NePQYEA`BdY1@a>pQepgfB zY_tk9L$Wt5OfL9A!r*Y*$2${0OI12(Bu%kyETly`9t`U#_*|rd(q02$XcDK&L2fYh zbpx8@M_{`v!CSH|fBl^Zjc|t8dk&ek&eL#)b3#W<8@_bMa3iEuE@W+f zCd;Z76z{>Fz0;guMp-35lE=u*WfFRg5!*hqWaEA^*APvL`q%8GdiSrfOa*@SLTcpb z$W01ULaI(&uU*r;R<`R(5Rg-8=TD@~3MZF(J%=aTKIhy!1O713u_}P zl}@E@DCVX9pCCH+Sj36s-xuV%n|DBg!T(0!L_{Bt=Y&SHe_u1^tD=R^)YZwuvF6Vq GXMY2+7CI6D diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 335769ff8b0ad99eb694df82ff65e208cb7d21df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2162 zcmcJQdpK148pnTYnPC{$#MlUl4xv5Ub&ShQ2VvVNNyzo6498_;928m7=D6F{u6@+D zCs7BvRW2!pbjsN&GkkDUWx1BNgG00~q8gdqUXe*%EQ zzS?ro7JiW8xVk$@%}Ak&x;6`jYeX1A|KjHC2*g>1-8}f+$JKG4chczGcv{|Y!jASO ztKq;z-Ebk%*N1H|l6j-tglXjVtEr?~^z*3vnX@N;s2J?JQ*kBV(4Qm%cdAFuejCmh z$~k|jH!nG7ILfl(pYdCmf4Nc|yJR){B}25dq4i|x$8}MgRv~Y_ZSt4RGbJf$DQ&;> zY#zdw_(wd)@|@Xa}7 zOx(;})x!o5Z-smcQwS<+Ww_TQnt6|T(KCWs`gApAZ(Bs_!j_Ww!+%!u{uXiz95__g|5Wq%6K!T6x)$l~K<~=9H)*nlIwCPuHD5 znq;`^5;Q?;s|7lecpjJ%4*1thP$+f#fgx%zeU+DqX#Qk?X(AVA(#YiIZ-Fan5dU!D zTq@eNjX5J7j85ob#>icfy$shG_@M^c*=VyTh(KAXrfSoSXgq7(Yk3>h8OCTvO!fxJ=-&&7bE?b|V6QIwF&23v&3`R;R2PT3D7SBx zE~HNpi*{q=Qcyr)7K4ZJHNl3MITD~HYbJ0ADFHr&&d+|Z#+oa&`vg)Z%}wy6cJF~s zNX+tAi`$^Xzuyz< zz;4@ZbK;H&v`eYI>dc&w-;I1HEfycS@1e-d0rty1Ny(U#6Ocw}AKTQ)=TfedxHA&0 zqjmR&WcH=*Kv^F^%h#_K1D>grZg}oRS&xA~z2FLMYK-{QCwA2C#h�Hz|lY2ROf7 zn%Bi1AyM>tl1?m;IFfIKc4@SakFSi=5pyrl$tXFrP_4|IA}lV?nF)CbC@T+SZpPT~ zrprfQ1``wptZvm@j(kt(CDnq{+P`o#Mm&rJJ;-bQ0_NNys zHqI`5Gn6$Cg3RaQTP?`Gy4YIO_=Zcv90uueizAdbTdTd7#Op^;hxS9O(~`azODOsi z_Oin_4KLD?QNN`RF?~R%8VcbH^7JM6@r3$tY^XdTT=NePQYEA`BdY1@a>pQepgfB zY_tk9L$Wt5OfL9A!r*Y*$2${0OI12(Bu%kyETly`9t`U#_*|rd(q02$XcDK&L2fYh zbpx8@M_{`v!CSH|fBl^Zjc|t8dk&ek&eL#)b3#W<8@_bMa3iEuE@W+f zCd;Z76z{>Fz0;guMp-35lE=u*WfFRg5!*hqWaEA^*APvL`q%8GdiSrfOa*@SLTcpb z$W01ULaI(&uU*r;R<`R(5Rg-8=TD@~3MZF(J%=aTKIhy!1O713u_}P zl}@E@DCVX9pCCH+Sj36s-xuV%n|DBg!T(0!L_{Bt=Y&SHe_u1^tD=R^)YZwuvF6Vq GXMY2+7CI6D From 679c458d70c4edd4b59dc82fb57704dd8233a965 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 18:44:40 +0200 Subject: [PATCH 35/41] all icons like svg --- .../mipmap-hdpi/ic_launcher_adaptive_back.png | Bin 0 -> 5621 bytes .../mipmap-hdpi/ic_launcher_adaptive_fore.png | Bin 0 -> 4096 bytes .../mipmap-mdpi/ic_launcher_adaptive_back.png | Bin 0 -> 3103 bytes .../mipmap-mdpi/ic_launcher_adaptive_fore.png | Bin 0 -> 2559 bytes .../mipmap-xhdpi/ic_launcher_adaptive_back.png | Bin 0 -> 8717 bytes .../mipmap-xhdpi/ic_launcher_adaptive_fore.png | Bin 0 -> 6409 bytes .../mipmap-xxhdpi/ic_launcher_adaptive_back.png | Bin 0 -> 16004 bytes .../mipmap-xxhdpi/ic_launcher_adaptive_fore.png | Bin 0 -> 10377 bytes .../ic_launcher_adaptive_back.png | Bin 0 -> 25168 bytes .../ic_launcher_adaptive_fore.png | Bin 0 -> 15977 bytes 10 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png b/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png new file mode 100644 index 0000000000000000000000000000000000000000..a44d618bf304e5c963d59e33186c4c4e97754d2e GIT binary patch literal 5621 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 P00000NkvXXu0mjfWvGXM literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png b/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 0000000000000000000000000000000000000000..8d671b3c0a7799f4c643286c2dd693990500837b GIT binary patch literal 4096 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 Xr~48XL@#om^n9lfPH=|ph4}vfr1i=U literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png b/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png new file mode 100644 index 0000000000000000000000000000000000000000..43aaf7449ffaca4b165dbc159da2d9916c9282b5 GIT binary patch literal 3103 zcmV+)4B+#LP)Px=*-1n}RCr$PUFn+JHVj<7Thk`#m8MPlPHpa{eO>1>EpaIjzzj&rmfZEftat(# z%n+iT>FvL#(=Ns^L6Dmk#$LwupS`-xU%HM5_piWCms3Dk2?( zz#s)ko!%}8Gu+k`sbsXm#fx!Nlm@bb(R3Y@jH>_7 zILsIp&j&e75gn!qF%(jT3?w|}P#?kt0S+_M;(?Uy^ycg7l+F*DI-8N8sjJ+)GeX18 zHq@ay4!eFYeWUq0_iw383!&2+AnP-sI%(xr{FPJMDu*lQq(a(@rsejkj%o&A)bxhD z`-rg8rG(nq6k){tA?`sOW+a$W{av-v;i*IuVIWG%NX<HGpdz{Z#p|NYj-$& z)ty0fq62Y&miT4a ziX6i$glSP~ot&x(c*ERf5Suy$qwEf%5YOA5ewQk-;E2o>j(jxz01#%W zLqjZP_xEx@6xn7ivrO{>Vb*&$_YJu~hGkb3VOFGC;>1-gZ2eK7Cdyrw%5qhv^6(b? z#VW)L!u|=WME&Xpf2U#TPoBH1s=TElhrYLC-zpT5te%nBafk{}N{JP62%!&~M36#MPyg}B~6s#GRHA`nCRABB3a=Q`8^qcHbbyBsvfw z7h#aJE;EtM9@~Nr=3|h)R%;3HmiMN?LWH;P)Q6ovSV5Y8{aoLzU!Ai8-p?KhR`Zc+ zLzNNlcGngv(^7?zTTzO3p)7q79pr~@D8{oQPEOl{tfQhUnxPWHNPkhy(A|6JBteR3 zam>wR6A?=0syJCJpoP1t`R92VRW0o`++i!IBWol6BRH!WSg2|{EORq)ZOaBFmBgpujHuP*V{A=XDT>LMo8CPnn${=0o34YxD!%4LS^}DS9ef zHAEtmRi(_BO~@)B$IQ`do=SAQ{IF3iQHxBXQsGftde*Bdfo!ZEE1VU{44XJ?^q}T= z0e+&ys1o8#)E^O9Sr@*1Pn^Rxc*ATW32;=U60PW%$3c?HMN~sZ*gE*9b4naONh6X> zDGeSl8$hUX)#N+@GtY2aiUB8tu(A@6@7Q;Izp0g@Xk^U|Q3b<{fg7K*ki%mLD?)5! zw7Fv$583(e1zDNHUYt2>!%sy?BuByYC?FR>l=(huhn!W(`mG(Nr9d}6IuVt*2!tVn z>P;iL5hT-Shd}^=2C%=_X!k9wopaXJ5Jp9((}?O{?1tegMhUuY8c{K_+w}ZBsB|oE zl(la@I_ER%`(RPECo3ClEw4IX855iBMp}8`SWPK08vV=#jPRJ?7q?3r9hI*%KTpr! z8UItd7LgXBvgY(Jt+dqOv~!>ruq?7@6*>?dBZf*Tal}9jihB8hPb_9c5 z5Z3XDs=8__F#vm>GP#gYp&i@3dDm`-Sv%;8f_cygqV5@0rH!W_-d$8;gt9_!oIy-R zISQ&w4Y31jpSOls2w@{o<03LR0c?I?uCnZu5Y`nnOGFzGZADNBNk)d~+Sg7y_d6`O zu^b%@VM2AeLsV597P7fOl={9b@Qb8!Zm2qq21lz{glt|k-UMMiC@MD+MHEzutybDm zzv=tGPbc|nZCM3w@GW@AO`cq}FHP;pq}wh9B20~PD)eIur-u{`>3*^0?6!46*p{o) zD95cS-g%{>*VdamOcNEMMgPwcC2a*A298KdYJ-ckGM*(Pr#Sh?7d zF^;g5loE$76pS`@vY{(K#9$o4maPC)H}XB%A*^?UMP3{>=re4fLF>84VW#f}I_w?| tw6{GyIc#qp0qc2k*ptH!W`Tn(_ZyliKobD^Z|VR5002ovPDHLkV1kj2zgYkP literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png b/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 0000000000000000000000000000000000000000..3a361855002add0a706c66c951016404800a6679 GIT binary patch literal 2559 zcmbVO`#%%vA9sn9QRF&ziREr5Axm;S%w@C97?Qb6GIWNP$Tuov2$Q?cbQgpC} z9Of=YvW47^rrbq1)1UDD;dwr<*YkRw*XQ$of8OuU>q&5gS)T&P0JylgPC;!fk(?NK zyiW3S_7p#teoo*HMOvG4)eOijb8!hYLoLmm!mqRQ=qV0xk#0P=57?1iXGXJCdEEVg zPqwKl&{%Z#;G@^g*w`ICQCVkmtxiQHNeo6LT3huDAsOVuJqkpYy|Hl;;p>3NTG4T0w0qK%c{&zcA&V>>$yO6gp&mOA! z%eDKWwv2|kH8rY8=H}wSiff*Z&j%9}vV+D0JOy*M&OHZBy^_%eJGuH-o3e)%8nIjuq2!H9(uH6T4h||jW^FVzH}AgiKKl8iXykMGrI{?Nm<&~Zw6nxp z5p-NqT^F#GHk3P?J9)NIxgpVF&PoD(@8yz290*%1URLr{y z@Q5FiQB&KCvFtZ780HDpmo?EJD4P;QJT88mnn6Wx6!I-p*tO|blFTcTwh&JQ$;;;< z*r=$e68*k1yDCUMV!nWg#=9Kmf8-i<${%FH>{Q+4aShbofE1&mDDg#J0f(B>XT?-w z#bKWG%!|uy!b+A_=m2SeWWqU4^&p2=e}z5Lpi5~1TBeCOlB|pj(#YS1K9u67I&8W9)?Y)zg2!O$w8a#zpi3Cv6EJZuwEot0_7sdZ)z6&RdaZ|wwqQHbZQ zlBX98R_)YTp_3*gf(rRSx8olxW4M3z++Tp+U^PX-a$1da?$<2qwR<|rUG?$z|EI;y z3~ms>D};FJ+o5~;&gwO{ra)J*q0gU{qJFH$UT?bxy~S#q|7LShtam$6QoFD#QR=ly zPS7f|)$SJa@17cAA)%r5d3rqsr?`Fl=rAf`ZMLhn2T>B41oLWbZGBg^)s*`xawixP z{vFG|H1+z{hp{m$R2i%UPB9sNZV|u4Y>m`5@~rne6*N&BQ|gfSNBxb#((V++QN~Np z1pW?TPutk zri^O$EVKQ!x!(@z<>EcWMFOX*xIkTnb*;sDo^Z9Cjp99#gp{It>5 z%fw||tGralUFk*#PGoAk`!i}hHaC|?7Xq&mzXI8l_2*F|JKtFG)q!~iL4_QNk5r&5 zmu5QR$8p;5*#~@rOCu$AVF!D+A(7jGV%f{$Y#!COOA;Vc^e&{|+Zn?Q zGrqg7qBn0DIMtN=MBz%V{y7dlxlB|RjkLDQvr+9HyH`QROPrU(z~L#o!r90oFQ#Ek zj)$_L?58--)`*PLa+ikspnuu(zX!NChp2u~KG+3v zcKQr1ZO8r-bE9-(Ts4@YpBf6ZA%=Y`NkX^dZ?)R*fymEgNLVM^U zjJS(`_%5ER>0DL7@yjU3J5`P)Z-%3QJNC{3k=LCFWKAIixe_Gq^ZH=Jb~3z4TXXO3 zwWi(Odui|ZwFn(D3VEbtyhgJ#=VG6}t2y3vxU4&AAg<+WTDs892hR(NP(a-TQ84s? zZ0@mRvHFARQj0zlPZHxFo}NB61}+Efs`V~j-fOc}uh(@~FYb}7rbSbqb;C+nxfkOTZK40D5jYlJrDpznH{gjK?wAk9b%9{r3TU<>6(6$}_0s=sCt>b=`8YValLQ6Pijg!RTv72L_2)fxx*Fi-1&tLX5OOe0a6jZiF<|*E1 zFX>z{?V41*0A^*G-gxQjGT(MbVBx zv;kMX?GFTj_U;am7%>KJNHArqSnU&2xJ`@C{%Rd>y0VTtDV0DjRJr|#m|_!HkZfCn z&VLgP)nhfCG7U)hBGoJma--X;GBjW!v1b9&JrIyOG7;I}G0PdQ5AeW3HHzDUZPT|- z#ECNH(-P-@B89Xz=Z933iR9$4tDExCgyTfBJPVOcKyStJLXubc7q>2f3NULjc>+Jl z1tPyA07*naRCr$PUG3WBFm2trw^Aq+N}<3#m9Ijfx4V5NljL>$b&GAd@0pVF&;Fm)PdoChXc9Z5$$bGA6YAM;$x3wj0amnZn^b^enE;eea#_fBloh*A z6jJ1yvXu`MyBi>~&OqD5ntuHLpWnZ4%*8NR6Ip8?Bv8XLSpgV85!&q_EQ@SS#LgBc zgV-D8u}lUTGD`r=M;7$Zih-zzie&==hWQAH?=X*qKfQHSWw3De+M(m+3Pc7Ys!Fy8 zqN?P_mJc#1P%MhUz(CNCbkz;awdBXZT(|sK7>q%3Kv1&>OM25@J}rz9iKw}6%j{R1 z>Z4?n85{a6lNNFWW2lt^v%W{gcVIA;xj-B86;~9h0%}l^LXmIEHa?JTlL}BQD%X?2 zaHW;xh+{Ac8otWBs(6M#o>Qg8K7c}qlpTLqWNY?dh8}Zh54N2K3uU;4QM-AozeMt)Ebp8AG2h-dxBcCSbJ{Q0gOXSk|A=w!dWC$nX8#U!XJq-$N<>+ z_f#y4-$Z~p@N;1C#$IWlnf zV1z%t^+#{8+z8y=6;!5ll7SBLQHZ+wrTEg6eN$AWDu$?hM^y_^xfUiKidq;9Y}l{4 zsR??RYkM3+IslP6Mbg=W6?uA_n`kTt9yIyo=a4ykTnGuv_#ZgPqnB6iq_?$_7aUvj=O`r&UG; zy)Lsp-=YVDs0r9c)gOkYhW_haPY{KJ$z}G2<@`;n1-NE2ftLKjV9F_66l3kgVEF!* z`W%Eox*AdKE3adKnuw%CU~hgl8L`o$vJ6_yduIVayO}?h@7)!74#>Cp6thMajk5)2 zT^)KbxLCS7v4la3Jrzk+q#pU*B-NKaRx(jg%-DYwen<6RhKfPy!C*eJ^}5KDN>J>r z84Ms8art|{5#Tx1D(*r$o_~25`>;+L3`X~)$srOGUz6E;NH5%pEEtlAk7159-^y3n zs{mhFaFE$o2XKjqSgU~w5CJN7@5Wwt>c1%Py&Hpx{nwrH4c9N3>&Nf@@p>mmwYMUf zgxP~lzypX?(^Gmzw&y7rfb@%+2;rS4kTIM+*sgAP2TL{Yvj^)gM>~TxRu)`v4R_w1 zy(oscK~GRiznXh`j-wbXV6E*YNavy05C+R?Yo!*BWU$Ll5pUdsu{}Y-kOEUPc~4L8 znDsGxuykhxAV~y*%51Uv4NpAYd<26EHAr0pI}9Nx=WwNJ+1w-kv|?y)m%NT!!b4bk8eb zhPoc;I9EaTIat$f;{)1{HECrJMnVoL2Ir;_APf^CN3&7AK!mysyipc2)uU7l2=#1bJ&KJQJ^-QAgM z$+npLrtQWDiZ9_g7xKOIV1fH*H`Qe@;xZwP6dB`qdN1Z~9w4|Pl^#T`5=dW#60(*7&i8^?X|(xPtKX zyj8NuCWL0x)tJ1&OpIlKSA-0$WLWgNI+5%p+1&D-5R2vBF@K?Jx%Gy{FLd z4QMliHF_i!shX`8fmjNId45G@1cMcELyCH7(7nbgs8$ra{owq2Du~N^FluLuwQd>= zZNI@@R{ghSuPoPU{DnL)K-)J|5(;~%OKi7I1^~q}0Vw|@sb>$iuUo3vWsAKM2`Tdp zM57TkbFVNr2*Fe4QZiJ;B_KL8j{D8Fz0yOtDUmX%LXmJ0rwl6i(_Bx%>+kVf&s8ZH zy|MPDR*rNyR9KwswWcJBy;gcrmxRJ#CfkA%6io!6?3&&_gHe=&H<6`>$xjc5_FSDf z*+lLLO`g`(dcqMszz)b!%$?aW_g>AW_?QK{a&a)(VZ>7zOkG_B^SDi}2CQ~0%%#@; zhjK`8loHK^D4k?p4)TODA+q-Zv@9aeh9Q%EQVEI~^L}J7VuOH^oIic)A(-K2K+~@p zB9YHs7y?&Mtsrv+BPe@w12RP;kL-6b!kIKnq*~!Ll;R>fO(=Jy?27kGBJqP!|AW&}$RwHw2(w z!}&!|P^LsPAxfv!dlCLg_6M^U;chV#${fI09l3w}=6M%Ue>FYa7rQ`%p$<15l$!&c zMSZA|d*fyc&vVLo27)WKzuFT*g3AOvcMnH@W1Lx(1w#`Is^mYk2ZKAGiMS@S8ji5v zwbA*|GWM<`C{v;i1g)Y!7?koUt8o>brfg6pp|BUlj)`ANoKM>egKfFWcdBRSUd~`- zlL!$UGaWDK=@Z~ZsN zj70EtkWXPR3enTV8s=;ygQ@cX*SfQMI4~Hs2>{|&Z(wXamf_v;fI#X$-c2-B5V0F^ zs)$)5(t~NhFmM56ex7mnkYSc)+(f*T{ngGHH%UzUVpZ~QFwf{KT&jzxHzc&x5rDdl zfz_IyW(g;H7UK{B5+Yw&xEXux@d`meQAhvv^vA!!YKW9DNDc7`f`Qw%$v{y(M{6^Q zzm^igU=*S!rD}0n*U2Via%WFiOozZ=Ox$;4?4k!t4>{nfju2Q80V0Pu7PL2jxh#lz z8G8`}!yMkY_d0|}1_H$bDu~B#{!aWxbIwRxq8mL`1Vh!3zxf=67!V?LFk_|ut34Nk z+`Ii6qY?(x3P8=@E+1V{q`lV-zyEbEgE{M0L}CzuqES0>Xdjz$;?TCBu!>tC;t-4d zLI)U;#xPbP%`qAuNIu=@{$V=7kioVZxAv}y{cTeLH!)hP4gw$)cv)5vxXTcDi|i6` z+{}OVrbNN?(fKekhKIqa^d^2$@~co4c)+(S6#L^CY%p78} z|4{Z?V^_m}XZA8q_p)jp|M~a9cM}sQJyV848nQs9ZB+tP5mN3Kvly(t7J9&tJkAL>H^}KM+6tv7 zhH(c}5<6*)-e8b`pg@01yK816B6=CCrq6)aDR&{W=bMaQX*_$KM=(FN*elOrq2B6H z6?DLlTil0%6|YYh@)lg1Y6lD$Suiw%PzS|tE2Rpv5<6v%{>C=*?u};Ljl9u$VkT=lrYB{? z#UPsGBa#ZFST`Okb4lzFfR=YfFck4z zi|%<446OcG-vR}DCxNj9qLjZ84E<~*RJ}=Z7Ne^3bv1Ks>6JGaTED%|O^84$kz&yO z-HyQ^trEaoRfZrOAViKiGW1@jv6oOTP;m#%i`0Y3jFniYZ@*v7V8Zw`GXyb2Q6-Oj z_kH$8WfQ}arH0(=B$Wr#d!=q1a!6Dd5QPPSAq+;LMaxM+SrCOOQS15CpyWpoN)}@h zP7oq*!CpIY2?0UTpn1s`+T+)M)4mVZ`px9-|HI||B>(_Mu=EnOJ~t|uL%Ca+K)*L0 z&t1bR12r6y8D&k-p`<1%0eYN-v6MGF7xoFpQkYs@fD(g26i`POYUn2ATgI#{4x=49 zCqW2V9glE=5Si?iuM6XvNY%a=EU%q=wRDbD=-+~)Uc@h$N4$OAt6Z78d)>Ec8J<~J z8H|A3(M(ZW9bQh6gveyCz!B`eXRoC$GkVDlLYpz7no-ixY?PGGO6A3K9FK&>*EaetJhOG2?;#Ls<#Ee?|y<@;8d184VK zk#Eh~K`iha!qy(TSwRp?7P3woMnW0Pf`Spj9mWlCLN-xRkZojT`XMi*F;c7mFpFMS zXR_Cxa20_e68YIO+`YxESyR%mofGKz6dfmkQ47MS}X7S{n%FR?l z&F(?Gcj>u`X(Sn|HvQeTSYs?=@iyJ5eGqUrlyIpl;@@Ci4;uQT7z?6sZ^t6mG325^ zQ2|y3(L?WL(lgvQwB~y8Pjr$JPlPCW!+ho{{tXt%U>=Itdbs}Ak;;_ykgxFzo|e47 zErT^Glh%tC)!ysVo_ob_Fw1I7bUX;I3~ffNilW(8FKz*~P2(HC;|1O7IAihili2H- zyT`Bo=ll2PI}f_QV41&d2>g3*@PyycRW?g13tI%hP#fLk#yrMrph-T>E2PS0lgMP&d0CXAPtjZ)iT(s+tr)w1((;( zV9X7Q{WqEOn-wbq?r;AQZi7P)fq=|70^$XQGa*Wc-ZGlt@D0W=67^0Dk)e9xT8i2|+u}Eb*#2m6@_URW z^fD4-b(&I@8LT%oQhyH_1o>9|^Q;9%clZWloRC<+Vz9A_;o=s@s@f9=5yiheyx_!8 zY@LdIq8|HFxw#R0Jp&%T!59XU*0E^NNicE|7z(c?Aki35q^2j=oH3Dg~s%f+M^FeQF7 z_F5S1m(RP1jVovD|8KKRJx|&orr8u|*WpJamiIZA>>q*5D%M!usnMIB?)U+Wby!D$ zS>^mH$qSG@g~2HF@dN_~7%_g;rePq1$}3Z=qFEo2$SCIef!NE;U=m~H8^^+6%ZC~!tsI`UR(~cxS7E`$SHeS+oT;MqkL=H z6$cc>kCBdMF2FEW-xobmnHJ9kX+8Hs#0DO}{OkG~%{kPm|L)bwsF?$sMIa0uaVD`d z!`Avi2<;CctAo6$4A8`)-Z+(zQgNo|s=hH;C@Grb5ezF&`iTn5A;*d1=|chUMmrzH zS_T0B9Og0vPtWU(+3VGVoEg(|)@$Y|)yNgJU87fwLj+MXts(7oD-~rjn zv@O;c%kkbqSl)^dFE7y5zV&8F(>;*rCPNEzd4=e)L=5&op1k1kX{2*F>QD{uw<-W% zW)wJVjmO$|Q9i#Rb8U&I7;G2LdIaG?{jjZ5>#;^Nm=sK}=Q4aF`C}O@EnPwHCFTEO z;Vy$&(d^=wmf@lAP@rG+g~Yh};BaMk1KJqGTzjse12UNw44>}7ko=!uWMQxq#-WFB z^;8Z@HTPOQ#o+fAHs1k49Alb$m{C0!V61oCOtnRRyA4L~#6*xn2ukR^SSy05J!u)< zcfE1%b?Ct?i+Yrfsiz_kyyY(-Ago(>1>`O=-yxjW92V}iexYeipvQmzK({oUP1Wcchi{11dWj(fa7VLRY z5;6y@zl@psaTv0f-$w6+Tz==zV;Jn>EqcoE>mVb>qHGdEga}-#yA7i;TCE~T>%u7R zBXC;=BY{xQq{3hU-`DCf@$wn)CqH1QiAzbSG1wNTp{azMqTz7*-;)1tCVaaMhVlm| z6d7PYPX-z>CT)`tBt&4;leu7uOak%-v-C*l@X_SC(xhcK6AXuBw2M(a7Gyrwqt&5P zpL2cCHR5M{omx|_oLXV&S z<@Hvn95K8S=j$I9Ki8-sO!p%dk{vM!pN0yND(O3#(2^mGQh4QdVU zu~LAd)-jnFQVb3`nG2>UL||dC%~F1dWH?mvJ0&)GhnoNa@4#RKr~&q(%TRk~kfvSH zF>lY6S1`rK28<;G3>)8@$+;OU3Q>pV?IgA`Eh-{`gb0(4Bg$+HWH7-`6uNm*y!SGw+UfkQiKj zE0y@ur} z3Zi&-7B69~2F*prr9s&}r3(gYF;=x#%6laAZ{NMrkPJdbVGx{XTHrWrG?}5{48|O* zCNh)3&SbC-eBBw@%)SYVf+)N7zAnK#jmzpct~}`Eog}z0*hymz83qy#6wq#3hyagc z9>h=~r=pXo@caRZ*D-zP&s*=lMqK8YL`6}Yn#`zvtIo`JXDV8htgKtBc*YAIgIS`Dfj6)TmHHxjyhgKaGc%@DG} z#XS~i+HS>Iv*!xmx|eUTh=@Ya)L_J*LWdj4TC?Ax(;hB(`37?z^0^JvbJ{cOnuR-` z8qS`}HMKS6%QqMsQ7Bs&jTzP05W}8prR6p~ct>e;+_H~vr11FJ^DZL!DhZQZ(lagk z7RG6v@)=$l!VK@4zc*nh`jpX?jH!GZ1{;EsPNPnBYIR-SYjqm5GvcR`^u|I%7;MYO zfw6MPq>l|Nr+~t9D{zSCdFLk8yq$hZC(=G()BPDS$w6{-#7X1Mv)Y!FIi1 z$l{z~`HumPxJ=G|>+O%1d2ND|xUj{kmGCNr-c*cF^9zXrl;Jb*4e*u86N1GwO#8CQZg$C1O7WF2g z_H4rs?NI}A@FSbJoWWdZeZtJ$p0PT;-`#V2!t`A}#1IDC>(4QLzm_x)g9T9TWH4>I zK217=!9E2k&tR+cu*bESX0PQ+-&x8FgPk=PHI8`fcy>>f$8ZHh1rRGW6;sHcf%|PR ze@xKAnM^gBu{tdn%)=BIpMm>tFhdWu{nDf77a!TwM?c5PA6CouS;wc`DH*u`26LpL zsC`c?hC_zx7zwfO$n8|+GVu86AG6R&p5U7=9DB9 z++&KABZ_;&occG(z$FZJS7xfsNhn)A2bhd?lcj%-8w-PNZ$H`@Xzh9Ee{a6#R}5}& z)r~hOpEK2Slq)g-8O)5}X2mPg@EUW7%agILo6sAK{9>>-{-Y2| zHB82U@P#Y)spA{Z#dN*Qz}s&y%cKrN6kvGRxRxYLol`RKcEmILCIA2dyGcYrR2ytq zPqn36WVGcuANX~eQ)Y4wa?1?7-3DW`IGinr0SFN`oGJIF85q97L_$~&8ibbT>&>=C z20|wrjQ?*s_tWhYGBA9Djf&w^b|3~LL`J1z+P!fG+#Af&6R|xD^%yO}fo*~+1Imng zk>rEEYBPKu34&$RhQ8F>68y^9EL8VtFp5y3#hZTs0%z0vCY09S`I z*lm%Xbc_`HfTb`)7?FX6!CEZ{)dP9GYB8ASR8aiJuyktajd$w*P#HLEu#byeq#sfI zcY)r*{T-bq4+)T-hV6WuITD00000NkvXXu0mjfx#(tY literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 0000000000000000000000000000000000000000..45a6a5a60cdc6ca662329680cff9ebfed114cb45 GIT binary patch literal 6409 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 O4p33hL{`BqL;nxBgPNBB literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 0000000000000000000000000000000000000000..dba13791e06bfe57a3e8ee5125d1f0fbde0b7cd4 GIT binary patch literal 10377 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png new file mode 100644 index 0000000000000000000000000000000000000000..49a37c8e3e944d11f2eb15df56c0d62dbc0fcce7 GIT binary patch literal 25168 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�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 Date: Sat, 12 Jul 2025 23:06:16 +0200 Subject: [PATCH 36/41] known issues added to 0.6 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e8bd3aa..87c5b026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved README documentation with updated clone URLs - Enhanced logging throughout the application for better debugging +### Known Issues +- reinstallation via apk does not work in some or all cases, it is advised to uninstall previous version first ## [0.5.1] - 2025-07-10 ### Added From c8c1f21d3c110e4b31948baae3bf05f98676c879 Mon Sep 17 00:00:00 2001 From: Comodore125 Date: Sat, 12 Jul 2025 23:12:56 +0200 Subject: [PATCH 37/41] Update CHANGELOG.md more precise text --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87c5b026..fdd0a20b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Enhanced logging throughout the application for better debugging ### Known Issues -- reinstallation via apk does not work in some or all cases, it is advised to uninstall previous version first +- reinstallation via apk does not work in all cases, it is advised to uninstall previous version first. root cause: https://github.com/permissionlesstech/bitchat-android/issues/92#issuecomment-3066035548 + ## [0.5.1] - 2025-07-10 ### Added From 3a01de655fa300cb810db00bb6596d6aa14b10db Mon Sep 17 00:00:00 2001 From: Comodore125 Date: Sat, 12 Jul 2025 23:19:10 +0200 Subject: [PATCH 38/41] Update CHANGELOG.md even more precise and more elegant update --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdd0a20b..02357fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,8 +46,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Enhanced logging throughout the application for better debugging ### Known Issues -- reinstallation via apk does not work in all cases, it is advised to uninstall previous version first. root cause: https://github.com/permissionlesstech/bitchat-android/issues/92#issuecomment-3066035548 - +- Reinstallation via APK does not work in all cases; it is recommended to uninstall the previous version first. + - Root cause: [See issue #92, comment](https://github.com/permissionlesstech/bitchat-android/issues/92#issuecomment-3066035548) + ## [0.5.1] - 2025-07-10 ### Added From 78dc9dc026c697106bef6d3313ef7c375a74063f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 13 Jul 2025 17:09:53 +0200 Subject: [PATCH 39/41] Revert "known issues added to 0.6" --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02357fe1..6e8bd3aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,10 +45,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved README documentation with updated clone URLs - Enhanced logging throughout the application for better debugging -### Known Issues -- Reinstallation via APK does not work in all cases; it is recommended to uninstall the previous version first. - - Root cause: [See issue #92, comment](https://github.com/permissionlesstech/bitchat-android/issues/92#issuecomment-3066035548) - ## [0.5.1] - 2025-07-10 ### Added From 2a0f84b53b7695c67445d32b77ab60f6377667b2 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 13 Jul 2025 17:16:31 +0200 Subject: [PATCH 40/41] adjust scan intervals and reduce logging --- .../java/com/bitchat/android/MainActivity.kt | 2 +- .../mesh/BluetoothConnectionManager.kt | 27 +++----- .../android/mesh/BluetoothMeshService.kt | 4 +- .../com/bitchat/android/mesh/PowerManager.kt | 66 +++++++++---------- .../onboarding/BluetoothStatusManager.kt | 4 +- 5 files changed, 47 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 5997a9f1..952a44a0 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -189,7 +189,7 @@ class MainActivity : ComponentActivity() { * Check Bluetooth status and proceed with onboarding flow */ private fun checkBluetoothAndProceed() { - android.util.Log.d("MainActivity", "Checking Bluetooth status") + // android.util.Log.d("MainActivity", "Checking Bluetooth status") // For first-time users, skip Bluetooth check and go straight to permissions // We'll check Bluetooth after permissions are granted diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index 7fb2ce14..10b78012 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -148,7 +148,7 @@ class BluetoothConnectionManager( startPeriodicCleanup() - Log.i(TAG, "Power-optimized Bluetooth services started successfully") + Log.i(TAG, "Bluetooth services started successfully") } return true @@ -254,7 +254,8 @@ class BluetoothConnectionManager( */ fun getDebugInfo(): String { return buildString { - appendLine("=== Power-Optimized Bluetooth Connection Manager ===") + appendLine("=== Bluetooth Connection Manager ===") + appendLine("Bluetooth MAC Address: ${bluetoothAdapter?.address}") appendLine("Active: $isActive") appendLine("Bluetooth Enabled: ${bluetoothAdapter?.isEnabled}") appendLine("Has Permissions: ${hasBluetoothPermissions()}") @@ -586,7 +587,6 @@ class BluetoothConnectionManager( // DEBUG: Log ALL scan results first val device = result.device val rssi = result.rssi - val scanRecord = result.scanRecord Log.d(TAG, "Scan result: device: ${device.address}, Name: '${device.name}', RSSI: $rssi") handleScanResult(result) } @@ -675,7 +675,7 @@ class BluetoothConnectionManager( null } - Log.d(TAG, "Processing bitchat device: $deviceAddress, name: '$deviceName', peerID: $extractedPeerID, RSSI: $rssi") + // Log.d(TAG, "Processing bitchat device: $deviceAddress, name: '$deviceName', peerID: $extractedPeerID, RSSI: $rssi") // Power-aware RSSI filtering if (rssi < powerManager.getRSSIThreshold()) { @@ -686,7 +686,7 @@ class BluetoothConnectionManager( // CRITICAL FIX: Prevent multiple simultaneous connections to same device // Check if already connected OR already attempting to connect if (connectedDevices.containsKey(deviceAddress)) { - Log.d(TAG, "Device $deviceAddress already connected, skipping") + // Log.d(TAG, "Device $deviceAddress already connected, skipping") return } @@ -717,12 +717,6 @@ class BluetoothConnectionManager( val attempts = (currentAttempt?.attempts ?: 0) + 1 pendingConnections[deviceAddress] = ConnectionAttempt(attempts) - if (extractedPeerID != null) { - Log.i(TAG, "Initiating connection to peer $extractedPeerID at $deviceAddress (RSSI: $rssi, attempt: $attempts)") - } else { - Log.i(TAG, "Initiating connection to device with bitchat service at $deviceAddress (RSSI: $rssi, attempt: $attempts)") - } - // Start connection immediately while holding lock connectToDevice(device, rssi) } @@ -733,6 +727,7 @@ class BluetoothConnectionManager( if (!hasBluetoothPermissions()) return val deviceAddress = device.address + Log.d(TAG, "Connecting to device: $deviceAddress") val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { @@ -795,9 +790,7 @@ class BluetoothConnectionManager( } } - override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { - Log.d(TAG, "Client: Service discovery completed for $deviceAddress with status: $status") - + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { val service = gatt.getService(SERVICE_UUID) if (service != null) { @@ -854,16 +847,16 @@ class BluetoothConnectionManager( } try { - Log.d(TAG, "Attempting GATT connection to $deviceAddress with autoConnect=false") + Log.d(TAG, "Client: Attempting GATT connection to $deviceAddress with autoConnect=false") val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) if (gatt == null) { Log.e(TAG, "connectGatt returned null for $deviceAddress") pendingConnections.remove(deviceAddress) } else { - Log.d(TAG, "GATT connection initiated successfully for $deviceAddress") + Log.d(TAG, "Client: GATT connection initiated successfully for $deviceAddress") } } catch (e: Exception) { - Log.e(TAG, "Exception connecting to $deviceAddress: ${e.message}") + Log.e(TAG, "Client: Exception connecting to $deviceAddress: ${e.message}") pendingConnections.remove(deviceAddress) } } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 7732fd0f..85ba999d 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -291,9 +291,7 @@ class BluetoothMeshService(private val context: Context) { Log.i(TAG, "Starting Bluetooth mesh service with peer ID: $myPeerID") if (connectionManager.startServices()) { - isActive = true - Log.i(TAG, "Bluetooth services started successfully") - + isActive = true // Send initial announcements after services are ready serviceScope.launch { delay(1000) diff --git a/app/src/main/java/com/bitchat/android/mesh/PowerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PowerManager.kt index 2c8bcab0..2d879269 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PowerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PowerManager.kt @@ -26,12 +26,12 @@ class PowerManager(private val context: Context) { private const val MEDIUM_BATTERY = 50 // Scan duty cycle periods (ms) - private const val SCAN_ON_DURATION_NORMAL = 2000L // 2 seconds on - private const val SCAN_OFF_DURATION_NORMAL = 8000L // 8 seconds off - private const val SCAN_ON_DURATION_POWER_SAVE = 1000L // 1 second on - private const val SCAN_OFF_DURATION_POWER_SAVE = 15000L // 15 seconds off - private const val SCAN_ON_DURATION_ULTRA_LOW = 500L // 0.5 seconds on - private const val SCAN_OFF_DURATION_ULTRA_LOW = 30000L // 30 seconds off + private const val SCAN_ON_DURATION_NORMAL = 8000L // 8 seconds on + private const val SCAN_OFF_DURATION_NORMAL = 2000L // 2 seconds off + private const val SCAN_ON_DURATION_POWER_SAVE = 2000L // 2 seconds on + private const val SCAN_OFF_DURATION_POWER_SAVE = 8000L // 8 seconds off + private const val SCAN_ON_DURATION_ULTRA_LOW = 1000L // 1 second on + private const val SCAN_OFF_DURATION_ULTRA_LOW = 10000L // 10 seconds off // Connection limits private const val MAX_CONNECTIONS_NORMAL = 8 @@ -112,38 +112,38 @@ class PowerManager(private val context: Context) { /** * Get scan settings optimized for current power mode */ - fun getScanSettings(): ScanSettings { - // CRITICAL FIX: Set reportDelay to 0 for all modes. - // When using a custom duty cycle, we want scan results delivered immediately, - // not batched. A non-zero report delay can conflict with the scan window, - // causing missed results if the scan stops before the delay is met. - val builder = ScanSettings.Builder() - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + fun getScanSettings(): ScanSettings { + // CRITICAL FIX: Set reportDelay to 0 for all modes. + // When using a custom duty cycle, we want scan results delivered immediately, + // not batched. A non-zero report delay can conflict with the scan window, + // causing missed results if the scan stops before the delay is met. + val builder = ScanSettings.Builder() + .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - when (currentMode) { - PowerMode.PERFORMANCE -> builder - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) - .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) + when (currentMode) { + PowerMode.PERFORMANCE -> builder + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) + .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) - PowerMode.BALANCED -> builder - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) - .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) + PowerMode.BALANCED -> builder + .setScanMode(ScanSettings.SCAN_MODE_BALANCED) + .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) + .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) - PowerMode.POWER_SAVER -> builder - .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) - .setMatchMode(ScanSettings.MATCH_MODE_STICKY) - .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) + PowerMode.POWER_SAVER -> builder + .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + .setMatchMode(ScanSettings.MATCH_MODE_STICKY) + .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) - PowerMode.ULTRA_LOW_POWER -> builder - .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC) - .setMatchMode(ScanSettings.MATCH_MODE_STICKY) - .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) - } - - return builder.setReportDelay(0).build() + PowerMode.ULTRA_LOW_POWER -> builder + .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC) + .setMatchMode(ScanSettings.MATCH_MODE_STICKY) + .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) } + + return builder.setReportDelay(0).build() + } /** * Get advertising settings optimized for current power mode diff --git a/app/src/main/java/com/bitchat/android/onboarding/BluetoothStatusManager.kt b/app/src/main/java/com/bitchat/android/onboarding/BluetoothStatusManager.kt index b580ccdf..54c00ecc 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/BluetoothStatusManager.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/BluetoothStatusManager.kt @@ -91,7 +91,7 @@ class BluetoothStatusManager( * This should be called on every app startup */ fun checkBluetoothStatus(): BluetoothStatus { - Log.d(TAG, "Checking Bluetooth status") + // Log.d(TAG, "Checking Bluetooth status") return when { bluetoothAdapter == null -> { @@ -103,7 +103,7 @@ class BluetoothStatusManager( BluetoothStatus.DISABLED } else -> { - Log.d(TAG, "Bluetooth is enabled and ready") + // Log.d(TAG, "Bluetooth is enabled and ready") BluetoothStatus.ENABLED } } From 930081bba0f98215f944fde840049880312faf1c Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 13 Jul 2025 20:27:43 +0200 Subject: [PATCH 41/41] add location services check --- .../java/com/bitchat/android/MainActivity.kt | 155 ++++++++- .../mesh/BluetoothConnectionManager.kt | 8 +- .../android/onboarding/LocationCheckScreen.kt | 297 ++++++++++++++++++ .../onboarding/LocationStatusManager.kt | 247 +++++++++++++++ 4 files changed, 687 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/onboarding/LocationCheckScreen.kt create mode 100644 app/src/main/java/com/bitchat/android/onboarding/LocationStatusManager.kt diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 54e81dbd..1ed97a92 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -33,6 +33,7 @@ class MainActivity : ComponentActivity() { private lateinit var permissionManager: PermissionManager private lateinit var onboardingCoordinator: OnboardingCoordinator private lateinit var bluetoothStatusManager: BluetoothStatusManager + private lateinit var locationStatusManager: LocationStatusManager // Core mesh service - managed at app level private lateinit var meshService: BluetoothMeshService @@ -48,12 +49,15 @@ class MainActivity : ComponentActivity() { // UI state for onboarding flow private var onboardingState by mutableStateOf(OnboardingState.CHECKING) private var bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED) + private var locationStatus by mutableStateOf(LocationStatus.ENABLED) private var errorMessage by mutableStateOf("") private var isBluetoothLoading by mutableStateOf(false) + private var isLocationLoading by mutableStateOf(false) enum class OnboardingState { CHECKING, BLUETOOTH_CHECK, + LOCATION_CHECK, PERMISSION_EXPLANATION, PERMISSION_REQUESTING, INITIALIZING, @@ -75,6 +79,12 @@ class MainActivity : ComponentActivity() { onBluetoothEnabled = ::handleBluetoothEnabled, onBluetoothDisabled = ::handleBluetoothDisabled ) + locationStatusManager = LocationStatusManager( + activity = this, + context = this, + onLocationEnabled = ::handleLocationEnabled, + onLocationDisabled = ::handleLocationDisabled + ) onboardingCoordinator = OnboardingCoordinator( activity = this, permissionManager = permissionManager, @@ -118,6 +128,20 @@ class MainActivity : ComponentActivity() { ) } + OnboardingState.LOCATION_CHECK -> { + LocationCheckScreen( + status = locationStatus, + onEnableLocation = { + isLocationLoading = true + locationStatusManager.requestEnableLocation() + }, + onRetry = { + checkLocationAndProceed() + }, + isLoading = isLocationLoading + ) + } + OnboardingState.PERMISSION_EXPLANATION -> { PermissionExplanationScreen( permissionCategories = permissionManager.getCategorizedPermissions(), @@ -205,8 +229,8 @@ class MainActivity : ComponentActivity() { when (bluetoothStatus) { BluetoothStatus.ENABLED -> { - // Bluetooth is enabled, proceed with permission/onboarding check - proceedWithPermissionCheck() + // Bluetooth is enabled, check location services next + checkLocationAndProceed() } BluetoothStatus.DISABLED -> { // Show Bluetooth enable screen (should have permissions as existing user) @@ -253,8 +277,77 @@ class MainActivity : ComponentActivity() { android.util.Log.d("MainActivity", "Bluetooth enabled by user") isBluetoothLoading = false bluetoothStatus = BluetoothStatus.ENABLED + checkLocationAndProceed() + } + + /** + * Check Location services status and proceed with onboarding flow + */ + private fun checkLocationAndProceed() { + android.util.Log.d("MainActivity", "Checking location services status") + + // For first-time users, skip location check and go straight to permissions + // We'll check location after permissions are granted + if (permissionManager.isFirstTimeLaunch()) { + android.util.Log.d("MainActivity", "First-time launch, skipping location check - will check after permissions") + proceedWithPermissionCheck() + return + } + + // For existing users, check location status + locationStatusManager.logLocationStatus() + locationStatus = locationStatusManager.checkLocationStatus() + + when (locationStatus) { + LocationStatus.ENABLED -> { + // Location services enabled, proceed with permission/onboarding check + proceedWithPermissionCheck() + } + LocationStatus.DISABLED -> { + // Show location enable screen (should have permissions as existing user) + android.util.Log.d("MainActivity", "Location services disabled, showing enable screen") + onboardingState = OnboardingState.LOCATION_CHECK + isLocationLoading = false + } + LocationStatus.NOT_AVAILABLE -> { + // Device doesn't support location services (very unusual) + android.util.Log.e("MainActivity", "Location services not available") + onboardingState = OnboardingState.LOCATION_CHECK + isLocationLoading = false + } + } + } + + /** + * Handle Location enabled callback + */ + private fun handleLocationEnabled() { + android.util.Log.d("MainActivity", "Location services enabled by user") + isLocationLoading = false + locationStatus = LocationStatus.ENABLED proceedWithPermissionCheck() } + + /** + * Handle Location disabled callback + */ + private fun handleLocationDisabled(message: String) { + android.util.Log.w("MainActivity", "Location services disabled or failed: $message") + isLocationLoading = false + locationStatus = locationStatusManager.checkLocationStatus() + + when { + locationStatus == LocationStatus.NOT_AVAILABLE -> { + // Show permanent error for devices without location services + errorMessage = message + onboardingState = OnboardingState.ERROR + } + else -> { + // Stay on location check screen for retry + onboardingState = OnboardingState.LOCATION_CHECK + } + } + } /** * Handle Bluetooth disabled callback @@ -289,20 +382,33 @@ class MainActivity : ComponentActivity() { } private fun handleOnboardingComplete() { - android.util.Log.d("MainActivity", "Onboarding completed, checking Bluetooth again before initializing app") + android.util.Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app") - // After permissions are granted, re-check Bluetooth status + // After permissions are granted, re-check both Bluetooth and Location status val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() - if (currentBluetoothStatus == BluetoothStatus.ENABLED) { - // Bluetooth is enabled, proceed to app initialization - onboardingState = OnboardingState.INITIALIZING - initializeApp() - } else { - // Bluetooth still disabled, but now we have permissions to enable it - android.util.Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.") - bluetoothStatus = currentBluetoothStatus - onboardingState = OnboardingState.BLUETOOTH_CHECK - isBluetoothLoading = false + val currentLocationStatus = locationStatusManager.checkLocationStatus() + + when { + currentBluetoothStatus != BluetoothStatus.ENABLED -> { + // Bluetooth still disabled, but now we have permissions to enable it + android.util.Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.") + bluetoothStatus = currentBluetoothStatus + onboardingState = OnboardingState.BLUETOOTH_CHECK + isBluetoothLoading = false + } + currentLocationStatus != LocationStatus.ENABLED -> { + // Location services still disabled, but now we have permissions to enable it + android.util.Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.") + locationStatus = currentLocationStatus + onboardingState = OnboardingState.LOCATION_CHECK + isLocationLoading = false + } + else -> { + // Both are enabled, proceed to app initialization + android.util.Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization") + onboardingState = OnboardingState.INITIALIZING + initializeApp() + } } } @@ -363,7 +469,7 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() - // Check Bluetooth status on resume and handle accordingly + // Check Bluetooth and Location status on resume and handle accordingly if (onboardingState == OnboardingState.COMPLETE) { // Set app foreground state meshService.connectionManager.setAppBackgroundState(false) @@ -376,6 +482,16 @@ class MainActivity : ComponentActivity() { bluetoothStatus = currentBluetoothStatus onboardingState = OnboardingState.BLUETOOTH_CHECK isBluetoothLoading = false + return + } + + // Check if location services were disabled while app was backgrounded + val currentLocationStatus = locationStatusManager.checkLocationStatus() + if (currentLocationStatus != LocationStatus.ENABLED) { + android.util.Log.w("MainActivity", "Location services disabled while app was backgrounded") + locationStatus = currentLocationStatus + onboardingState = OnboardingState.LOCATION_CHECK + isLocationLoading = false } } } @@ -436,6 +552,15 @@ class MainActivity : ComponentActivity() { override fun onDestroy() { super.onDestroy() + + // Cleanup location status manager + try { + locationStatusManager.cleanup() + android.util.Log.d("MainActivity", "Location status manager cleaned up successfully") + } catch (e: Exception) { + android.util.Log.w("MainActivity", "Error cleaning up location status manager: ${e.message}") + } + // Stop mesh services if app was fully initialized if (onboardingState == OnboardingState.COMPLETE) { try { diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index 4f705023..9bffaa62 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -649,7 +649,7 @@ class BluetoothConnectionManager( // DEBUG: Log ALL scan results first val device = result.device val rssi = result.rssi - Log.d(TAG, "Scan result: device: ${device.address}, Name: '${device.name}', RSSI: $rssi") + // Log.d(TAG, "Scan result: device: ${device.address}, Name: '${device.name}', RSSI: $rssi") handleScanResult(result) } @@ -736,9 +736,7 @@ class BluetoothConnectionManager( } else { null } - - // Log.d(TAG, "Processing bitchat device: $deviceAddress, name: '$deviceName', peerID: $extractedPeerID, RSSI: $rssi") - + // Power-aware RSSI filtering if (rssi < powerManager.getRSSIThreshold()) { Log.d(TAG, "Skipping device $deviceAddress due to weak signal: $rssi < ${powerManager.getRSSIThreshold()}") @@ -789,7 +787,7 @@ class BluetoothConnectionManager( if (!hasBluetoothPermissions()) return val deviceAddress = device.address - Log.d(TAG, "Connecting to device: $deviceAddress") + Log.d(TAG, "Connecting to bitchat device: $deviceAddress") val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { diff --git a/app/src/main/java/com/bitchat/android/onboarding/LocationCheckScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/LocationCheckScreen.kt new file mode 100644 index 00000000..37fb072f --- /dev/null +++ b/app/src/main/java/com/bitchat/android/onboarding/LocationCheckScreen.kt @@ -0,0 +1,297 @@ +package com.bitchat.android.onboarding + +import androidx.compose.animation.core.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +/** + * Screen shown when checking location services status or requesting location services enable + */ +@Composable +fun LocationCheckScreen( + status: LocationStatus, + onEnableLocation: () -> Unit, + onRetry: () -> Unit, + isLoading: Boolean = false +) { + val colorScheme = MaterialTheme.colorScheme + + Box( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + when (status) { + LocationStatus.DISABLED -> { + LocationDisabledContent( + onEnableLocation = onEnableLocation, + onRetry = onRetry, + colorScheme = colorScheme, + isLoading = isLoading + ) + } + LocationStatus.NOT_AVAILABLE -> { + LocationNotAvailableContent( + colorScheme = colorScheme + ) + } + LocationStatus.ENABLED -> { + LocationCheckingContent( + colorScheme = colorScheme + ) + } + } + } +} + +@Composable +private fun LocationDisabledContent( + onEnableLocation: () -> Unit, + onRetry: () -> Unit, + colorScheme: ColorScheme, + isLoading: Boolean +) { + Column( + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Location icon - using LocationOn outlined icon in app's green color + Icon( + imageVector = Icons.Outlined.LocationOn, + contentDescription = "Location Services", + modifier = Modifier.size(64.dp), + tint = Color(0xFF00C851) // App's main green color + ) + + Text( + text = "Location Services Required", + style = MaterialTheme.typography.headlineSmall.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + color = colorScheme.primary + ), + textAlign = TextAlign.Center + ) + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Privacy assurance section + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = Icons.Filled.Security, + contentDescription = "Privacy", + tint = Color(0xFF4CAF50), + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Privacy First", + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold, + color = colorScheme.onSurface + ) + ) + } + + Text( + text = "bitchat does NOT track your location or use GPS.\n\nLocation services are required by Android for Bluetooth scanning to work properly. This is an Android system requirement.", + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + color = colorScheme.onSurface.copy(alpha = 0.8f) + ) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "bitchat needs location services for:", + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Text( + text = "• Bluetooth device scanning (Android requirement)\n" + + "• Discovering nearby users on mesh network\n" + + "• Creating connections without internet\n" + + "• No GPS tracking or location collection", + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + color = colorScheme.onSurface.copy(alpha = 0.8f) + ) + ) + } + } + + if (isLoading) { + LocationLoadingIndicator() + } else { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onEnableLocation, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF00C851) // App's main green color + ) + ) { + Text( + text = "Open Location Settings", + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold + ), + modifier = Modifier.padding(vertical = 4.dp) + ) + } + + OutlinedButton( + onClick = onRetry, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Check Again", + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace + ), + modifier = Modifier.padding(vertical = 4.dp) + ) + } + } + } + } +} + +@Composable +private fun LocationNotAvailableContent( + colorScheme: ColorScheme +) { + Column( + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Error icon + Icon( + imageVector = Icons.Filled.ErrorOutline, + contentDescription = "Error", + modifier = Modifier.size(64.dp), + tint = colorScheme.error + ) + + Text( + text = "Location Services Unavailable", + style = MaterialTheme.typography.headlineSmall.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + color = colorScheme.error + ), + textAlign = TextAlign.Center + ) + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = colorScheme.errorContainer.copy(alpha = 0.1f) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Text( + text = "Location services are not available on this device. This is unusual as location services are standard on Android devices.\n\nbitchat needs location services for Bluetooth scanning to work properly (Android requirement). Without this, the app cannot discover nearby users.", + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace, + color = colorScheme.onSurface + ), + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Center + ) + } + } +} + +@Composable +private fun LocationCheckingContent( + colorScheme: ColorScheme +) { + Column( + verticalArrangement = Arrangement.spacedBy(32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "bitchat*", + style = MaterialTheme.typography.headlineLarge.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + color = colorScheme.primary + ), + textAlign = TextAlign.Center + ) + + LocationLoadingIndicator() + + Text( + text = "Checking location services...", + style = MaterialTheme.typography.bodyLarge.copy( + fontFamily = FontFamily.Monospace, + color = colorScheme.onSurface.copy(alpha = 0.7f) + ) + ) + } +} + +@Composable +private fun LocationLoadingIndicator() { + // Animated rotation for the loading indicator + val infiniteTransition = rememberInfiniteTransition(label = "location_loading") + val rotationAngle by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 2000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ), + label = "rotation" + ) + + Box( + modifier = Modifier.size(60.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier + .fillMaxSize() + .rotate(rotationAngle), + color = Color(0xFF4CAF50), // Location green + strokeWidth = 3.dp + ) + } +} diff --git a/app/src/main/java/com/bitchat/android/onboarding/LocationStatusManager.kt b/app/src/main/java/com/bitchat/android/onboarding/LocationStatusManager.kt new file mode 100644 index 00000000..30bd6910 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/onboarding/LocationStatusManager.kt @@ -0,0 +1,247 @@ +package com.bitchat.android.onboarding + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.location.LocationManager +import android.os.Build +import android.provider.Settings +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts + +/** + * Manages Location Services enable/disable state and user prompts + * Checks location services status on every app startup + * Note: This is for system location services, not location permissions + */ +class LocationStatusManager( + private val activity: ComponentActivity, + private val context: Context, + private val onLocationEnabled: () -> Unit, + private val onLocationDisabled: (String) -> Unit +) { + + companion object { + private const val TAG = "LocationStatusManager" + } + + private var locationSettingsLauncher: ActivityResultLauncher? = null + private var locationManager: LocationManager? = null + private var locationStateReceiver: BroadcastReceiver? = null + + init { + setupLocationManager() + setupLocationSettingsLauncher() + setupLocationStateReceiver() + } + + /** + * Setup LocationManager reference + */ + private fun setupLocationManager() { + try { + locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + Log.d(TAG, "LocationManager initialized: ${locationManager != null}") + } catch (e: Exception) { + Log.e(TAG, "Failed to initialize LocationManager", e) + locationManager = null + } + } + + /** + * Setup launcher for location settings request + */ + private fun setupLocationSettingsLauncher() { + locationSettingsLauncher = activity.registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val isEnabled = isLocationEnabled() + Log.d(TAG, "Location settings request result: $isEnabled (result code: ${result.resultCode})") + if (isEnabled) { + onLocationEnabled() + } else { + onLocationDisabled("Location services are required for Bluetooth scanning on Android. Please enable location services to continue.") + } + } + } + + /** + * Setup broadcast receiver to listen for location settings changes + */ + private fun setupLocationStateReceiver() { + locationStateReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == LocationManager.MODE_CHANGED_ACTION || + intent.action == LocationManager.PROVIDERS_CHANGED_ACTION) { + Log.d(TAG, "Location settings changed, checking status") + val isEnabled = isLocationEnabled() + if (isEnabled) { + onLocationEnabled() + } else { + onLocationDisabled("Location services have been disabled.") + } + } + } + } + + // Register receiver for location changes + val filter = IntentFilter().apply { + addAction(LocationManager.MODE_CHANGED_ACTION) + addAction(LocationManager.PROVIDERS_CHANGED_ACTION) + } + context.registerReceiver(locationStateReceiver, filter) + } + + /** + * Check if location services are enabled (system-wide setting) + * Uses proper API depending on Android version + */ + fun isLocationEnabled(): Boolean { + return try { + locationManager?.let { lm -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // API 28+ (Android 9) - Modern approach + lm.isLocationEnabled + } else { + // Older devices - Check individual providers + lm.isProviderEnabled(LocationManager.GPS_PROVIDER) || + lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } + } ?: false + } catch (e: Exception) { + Log.w(TAG, "Error checking location enabled state: ${e.message}") + false + } + } + + /** + * Check location services status + * This should be called on every app startup + */ + fun checkLocationStatus(): LocationStatus { + Log.d(TAG, "Checking location services status") + + return when { + locationManager == null -> { + Log.e(TAG, "LocationManager not available on this device") + LocationStatus.NOT_AVAILABLE + } + !isLocationEnabled() -> { + Log.w(TAG, "Location services are disabled") + LocationStatus.DISABLED + } + else -> { + Log.d(TAG, "Location services are enabled and ready") + LocationStatus.ENABLED + } + } + } + + /** + * Request user to enable location services + * Opens system location settings screen + */ + fun requestEnableLocation() { + Log.d(TAG, "Requesting user to enable location services") + + try { + val enableLocationIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + locationSettingsLauncher?.launch(enableLocationIntent) + } catch (e: Exception) { + Log.e(TAG, "Failed to request location enable", e) + onLocationDisabled("Failed to open location settings: ${e.message}") + } + } + + /** + * Handle location status check result + */ + fun handleLocationStatus(status: LocationStatus) { + when (status) { + LocationStatus.ENABLED -> { + Log.d(TAG, "Location services enabled, proceeding") + onLocationEnabled() + } + LocationStatus.DISABLED -> { + Log.d(TAG, "Location services disabled, requesting enable") + requestEnableLocation() + } + LocationStatus.NOT_AVAILABLE -> { + Log.e(TAG, "Location services not available") + onLocationDisabled("Location services are not available on this device.") + } + } + } + + /** + * Get user-friendly status message + */ + fun getStatusMessage(status: LocationStatus): String { + return when (status) { + LocationStatus.ENABLED -> "Location services are enabled and ready" + LocationStatus.DISABLED -> "Location services are disabled. Please enable location services for Bluetooth scanning." + LocationStatus.NOT_AVAILABLE -> "Location services are not available on this device." + } + } + + /** + * Get detailed diagnostics + */ + fun getDiagnostics(): String { + return buildString { + appendLine("Location Services Status Diagnostics:") + appendLine("LocationManager available: ${locationManager != null}") + appendLine("Location services enabled: ${isLocationEnabled()}") + appendLine("Current status: ${checkLocationStatus()}") + appendLine("Android version: ${Build.VERSION.SDK_INT}") + + locationManager?.let { lm -> + try { + appendLine("GPS provider enabled: ${lm.isProviderEnabled(LocationManager.GPS_PROVIDER)}") + appendLine("Network provider enabled: ${lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)}") + } catch (e: Exception) { + appendLine("Provider details: [Error: ${e.message}]") + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + appendLine("Using modern isLocationEnabled() API") + } else { + appendLine("Using legacy provider check API") + } + } + } + } + + /** + * Log current location status for debugging + */ + fun logLocationStatus() { + Log.d(TAG, getDiagnostics()) + } + + /** + * Cleanup resources - call this when activity is destroyed + */ + fun cleanup() { + locationStateReceiver?.let { receiver -> + try { + context.unregisterReceiver(receiver) + Log.d(TAG, "Location state receiver unregistered") + } catch (e: Exception) { + Log.w(TAG, "Error unregistering location state receiver: ${e.message}") + } + } + } +} + +/** + * Location services status enum + */ +enum class LocationStatus { + ENABLED, + DISABLED, + NOT_AVAILABLE +}