Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9e25ee4
Automated update of relay data - Sun Sep 21 06:21:05 UTC 2025
actions-user Sep 21, 2025
6cccaae
Merge remote-tracking branch 'origin/main'
yet300 Sep 24, 2025
95358ac
Automated update of relay data - Sun Sep 28 06:20:40 UTC 2025
actions-user Sep 28, 2025
696f698
refactor: new close button like ios(but not liquid glass)
yet300 Sep 30, 2025
e0c7240
Merge remote-tracking branch 'origin/main'
yet300 Sep 30, 2025
16d67d3
feat: Migrate from Parcelable to Kotlinx Serialization
yet300 Sep 30, 2025
ee08835
Merge branch 'permissionlesstech:main' into main
yet300 Sep 30, 2025
95e08d4
feat: Add JsonUtil to replace Gson
yet300 Sep 30, 2025
045db11
feat: Migrate from Gson to Kotlinx Serialization
yet300 Sep 30, 2025
866b8da
Refactor: Migrate Nostr JSON handling from Gson to kotlinx.serialization
yet300 Sep 30, 2025
d54fc17
Refactor: Use kotlinx.serialization for NoisePayload
yet300 Sep 30, 2025
d49ea7f
Refactor: Replace Gson with Kotlinx Serialization
yet300 Sep 30, 2025
af46f69
Refactor: Remove Gson dependency
yet300 Sep 30, 2025
148f804
Merge branch 'main' into feature/migrate-kotlinx.serialization
yet300 Oct 5, 2025
74d90fd
Merge branch 'main' into feature/migrate-kotlinx.serialization
yet300 Oct 13, 2025
314b8ea
Merge remote-tracking branch 'upstream/main' into feature/migrate-kot…
yet300 Oct 24, 2025
df5375f
Refactor: Replace CloseButton with TextButton in bottom sheets
yet300 Oct 25, 2025
d52f95f
feat: Add Koin for dependency injection
yet300 Nov 19, 2025
43209f6
Refactor Data Layer to Clean Architecture with Koin
yet300 Nov 21, 2025
3112aeb
refactor: migrate singleton services to Koin dependency injection
yet300 Nov 21, 2025
0a84982
refactor: migrate GeohashBookmarksStore and DebugSettingsManager to K…
yet300 Nov 21, 2025
d7283b5
refactor: inject DebugSettingsManager into mesh layer core classes
yet300 Nov 23, 2025
c077f4f
refactor: complete DebugSettingsManager Koin migration
yet300 Nov 23, 2025
4cec384
Refactor: Use Koin property delegation for meshService
yet300 Nov 23, 2025
328a0dd
Refactor: Use Koin for PermissionManager dependency injection
yet300 Nov 23, 2025
8ba831e
refactor: Add PeerFingerprintManager dependency to tests
yet300 Nov 23, 2025
b07a846
refactor: eliminate lateinit vars and use Koin dependency injection
yet300 Nov 23, 2025
776fcfb
Refactor: Lazily initialize GossipSyncManager
yet300 Nov 23, 2025
e76f34d
Refactor FavoritesPersistenceService to use Koin dependency injection
yet300 Nov 24, 2025
e0f6489
refactor: Use dependency injection for manager classes
yet300 Nov 24, 2025
29d0241
refactor: Centralize UI logic in ChatViewModel
yet300 Nov 25, 2025
26a8ad8
Refactor: Move geohash and PoW logic to ChatViewModel
yet300 Nov 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
}

android {
Expand Down Expand Up @@ -82,13 +83,13 @@ dependencies {

// Cryptography
implementation(libs.bundles.cryptography)

// JSON
implementation(libs.gson)


// Coroutines
implementation(libs.kotlinx.coroutines.android)

// Serialization
implementation(libs.kotlinx.serialization.json)

// Bluetooth
implementation(libs.nordic.ble)

Expand All @@ -112,4 +113,12 @@ dependencies {
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.bundles.compose.testing)
debugImplementation(libs.androidx.compose.ui.tooling)

// Koin
implementation(libs.koin.core)
implementation(libs.koin.android)
implementation(libs.koin.compose)
implementation(libs.koin.annotation)
implementation(libs.koin.jsr330)
ksp(libs.koin.annotation.compiler)
}
27 changes: 7 additions & 20 deletions app/src/main/java/com/bitchat/android/BitchatApplication.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.bitchat.android

import android.app.Application
import com.bitchat.android.nostr.RelayDirectory
import com.bitchat.android.di.initKoin
import com.bitchat.android.ui.theme.ThemePreferenceManager
import com.bitchat.android.net.TorManager
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger

/**
* Main application class for bitchat Android
Expand All @@ -12,20 +13,11 @@ class BitchatApplication : Application() {

override fun onCreate() {
super.onCreate()

// Initialize Tor first so any early network goes over Tor
try { TorManager.init(this) } catch (_: Exception) { }

// Initialize relay directory (loads assets/nostr_relays.csv)
RelayDirectory.initialize(this)

// Initialize LocationNotesManager dependencies early so sheet subscriptions can start immediately
try { com.bitchat.android.nostr.LocationNotesInitializer.initialize(this) } catch (_: Exception) { }

// Initialize favorites persistence early so MessageRouter/NostrTransport can use it on startup
try {
com.bitchat.android.favorites.FavoritesPersistenceService.initialize(this)
} catch (_: Exception) { }
initKoin{
androidContext(this@BitchatApplication)
androidLogger()
}

// Warm up Nostr identity to ensure npub is available for favorite notifications
try {
Expand All @@ -34,10 +26,5 @@ class BitchatApplication : Application() {

// Initialize theme preference
ThemePreferenceManager.init(this)

// Initialize debug preference manager (persists debug toggles)
try { com.bitchat.android.ui.debug.DebugPreferenceManager.init(this) } catch (_: Exception) { }

// TorManager already initialized above
}
}
28 changes: 7 additions & 21 deletions app/src/main/java/com/bitchat/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import org.koin.androidx.viewmodel.ext.android.viewModel
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
Expand Down Expand Up @@ -42,37 +43,29 @@ import com.bitchat.android.ui.theme.BitchatTheme
import com.bitchat.android.nostr.PoWPreferenceManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject

class MainActivity : OrientationAwareActivity() {

private lateinit var permissionManager: PermissionManager
private val permissionManager: PermissionManager by inject()
private lateinit var onboardingCoordinator: OnboardingCoordinator
private lateinit var bluetoothStatusManager: BluetoothStatusManager
private lateinit var locationStatusManager: LocationStatusManager
private lateinit var batteryOptimizationManager: BatteryOptimizationManager

// Core mesh service - managed at app level
private lateinit var meshService: BluetoothMeshService
private val meshService: BluetoothMeshService by inject()

private val mainViewModel: MainViewModel by viewModels()
private val chatViewModel: ChatViewModel by viewModels {
object : ViewModelProvider.Factory {
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return ChatViewModel(application, meshService) as T
}
}
}
private val chatViewModel: ChatViewModel by viewModel()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Enable edge-to-edge display for modern Android look
enableEdgeToEdge()

// Initialize permission management
permissionManager = PermissionManager(this)
// Initialize core mesh service first
meshService = BluetoothMeshService(this)
// Initialize core mesh service first - retrieve from Koin
bluetoothStatusManager = BluetoothStatusManager(
activity = this,
context = this,
Expand Down Expand Up @@ -593,13 +586,6 @@ class MainActivity : OrientationAwareActivity() {

Log.d("MainActivity", "Permissions verified, initializing chat system")

// Initialize PoW preferences early in the initialization process
PoWPreferenceManager.init(this@MainActivity)
Log.d("MainActivity", "PoW preferences initialized")

// Initialize Location Notes Manager (extracted to separate file)
com.bitchat.android.nostr.LocationNotesInitializer.initialize(this@MainActivity)

// Ensure all permissions are still granted (user might have revoked in settings)
if (!permissionManager.areAllPermissionsGranted()) {
val missing = permissionManager.getMissingPermissions()
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/bitchat/android/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import com.bitchat.android.onboarding.BatteryOptimizationStatus
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.koin.android.annotation.KoinViewModel

@KoinViewModel
class MainViewModel : ViewModel() {

private val _onboardingState = MutableStateFlow(OnboardingState.CHECKING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.bitchat.android.crypto
import android.content.Context
import android.util.Log
import com.bitchat.android.noise.NoiseEncryptionService
import com.bitchat.android.mesh.PeerFingerprintManager
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator
import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters
Expand All @@ -19,16 +22,17 @@ import java.util.concurrent.ConcurrentHashMap
* This is the main interface for all encryption/decryption operations in bitchat.
* It now uses the Noise protocol for secure transport encryption with proper session management.
*/
class EncryptionService(private val context: Context) {
@Singleton
class EncryptionService @Inject constructor(
private val context: Context,
private val noiseService: NoiseEncryptionService,
) {

companion object {
private const val TAG = "EncryptionService"
private const val ED25519_PRIVATE_KEY_PREF = "ed25519_signing_private_key"
}

// Core Noise encryption service
private val noiseService: NoiseEncryptionService = NoiseEncryptionService(context)

// Session tracking for established connections
private val establishedSessions = ConcurrentHashMap<String, String>() // peerID -> fingerprint

Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/bitchat/android/di/AppModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.bitchat.android.di

import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module

@Module
@ComponentScan("com.bitchat.android")
class AppModule
Comment on lines +6 to +8

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve missing Koin definitions for injected services

MainActivity now relies on Koin (by inject() for PermissionManager/BluetoothMeshService and by viewModel() for ChatViewModel), but the only module registered at startup is AppModule, which just component-scans com.bitchat.android without any Koin @Single/@Factory bindings. All the managers were annotated with jakarta.inject.Singleton instead of Koin’s annotations, so the generated module is effectively empty and Koin cannot provide the injected dependencies. At runtime the app will throw NoBeanDefFoundException as soon as MainActivity is created, blocking startup. Consider registering the bindings explicitly or switching the classes to Koin annotations so the component scan produces definitions.

Useful? React with 👍 / 👎.

14 changes: 14 additions & 0 deletions app/src/main/java/com/bitchat/android/di/InitKoin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.bitchat.android.di

import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration
import org.koin.ksp.generated.module

fun initKoin(config: KoinAppDeclaration? = null) {
startKoin {
modules(
AppModule().module
)
config?.invoke(this)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.bitchat.android.favorites

import android.content.Context
import android.util.Log
import com.bitchat.android.identity.SecureIdentityStateManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import com.bitchat.android.util.JsonUtil
import jakarta.inject.Inject
import jakarta.inject.Singleton
import java.util.*

/**
Expand Down Expand Up @@ -57,32 +60,18 @@ interface FavoritesChangeListener {
* Manages favorites with Noise↔Nostr mapping
* Singleton pattern matching iOS implementation.
*/
class FavoritesPersistenceService private constructor(private val context: Context) {
@Singleton
class FavoritesPersistenceService @Inject constructor(
private val stateManager : SecureIdentityStateManager
) {

companion object {
private const val TAG = "FavoritesPersistenceService"
private const val FAVORITES_KEY = "favorite_relationships" // noiseHex -> relationship
private const val PEERID_INDEX_KEY = "favorite_peerid_index" // peerID(16-hex) -> npub

@Volatile
private var INSTANCE: FavoritesPersistenceService? = null

val shared: FavoritesPersistenceService
get() = INSTANCE ?: throw IllegalStateException("FavoritesPersistenceService not initialized")

fun initialize(context: Context) {
if (INSTANCE == null) {
synchronized(this) {
if (INSTANCE == null) {
INSTANCE = FavoritesPersistenceService(context.applicationContext)
}
}
}
}
}

private val stateManager = SecureIdentityStateManager(context)
private val gson = Gson()

private val favorites = mutableMapOf<String, FavoriteRelationship>() // noiseHex -> relationship
// NEW: Index by current mesh peerID (16-hex) for direct lookup when sending Nostr DMs from mesh context
private val peerIdIndex = mutableMapOf<String, String>() // peerID (lowercase 16-hex) -> npub
Expand Down Expand Up @@ -161,7 +150,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
fun findPeerIDForNostrPubkey(nostrPubkey: String): String? {
// First, try direct match in peerIdIndex (values are stored as npub strings)
peerIdIndex.entries.firstOrNull { it.value.equals(nostrPubkey, ignoreCase = true) }?.let { return it.key }

// Attempt legacy mapping via favorites Noise key association
val targetHex = normalizeNostrKeyToHex(nostrPubkey)
if (targetHex != null) {
Expand Down Expand Up @@ -258,14 +247,14 @@ class FavoritesPersistenceService private constructor(private val context: Conte
try {
val favoritesJson = stateManager.getSecureValue(FAVORITES_KEY)
if (favoritesJson != null) {
val type = object : TypeToken<Map<String, FavoriteRelationshipData>>() {}.type
val data: Map<String, FavoriteRelationshipData> = gson.fromJson(favoritesJson, type)

favorites.clear()
data.forEach { (key, relationshipData) ->
favorites[key] = relationshipData.toFavoriteRelationship()
val data = JsonUtil.fromJsonOrNull(MapSerializer(String.serializer(), FavoriteRelationshipData.serializer()), favoritesJson)
if (data != null) {
favorites.clear()
data.forEach { (key, relationshipData) ->
favorites[key] = relationshipData.toFavoriteRelationship()
}
Log.d(TAG, "Loaded ${favorites.size} favorite relationships")
}
Log.d(TAG, "Loaded ${favorites.size} favorite relationships")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to load favorites: ${e.message}")
Expand All @@ -277,7 +266,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
val data = favorites.mapValues { (_, relationship) ->
FavoriteRelationshipData.fromFavoriteRelationship(relationship)
}
val favoritesJson = gson.toJson(data)
val favoritesJson = JsonUtil.toJson(MapSerializer(String.serializer(), FavoriteRelationshipData.serializer()), data)
stateManager.storeSecureValue(FAVORITES_KEY, favoritesJson)
Log.d(TAG, "Saved ${favorites.size} favorite relationships")
} catch (e: Exception) {
Expand All @@ -289,10 +278,11 @@ class FavoritesPersistenceService private constructor(private val context: Conte
try {
val json = stateManager.getSecureValue(PEERID_INDEX_KEY)
if (json != null) {
val type = object : TypeToken<Map<String, String>>() {}.type
val data: Map<String, String> = gson.fromJson(json, type)
peerIdIndex.clear()
peerIdIndex.putAll(data)
val data = JsonUtil.fromJsonOrNull(MapSerializer(String.serializer(), String.serializer()), json)
if (data != null) {
peerIdIndex.clear()
peerIdIndex.putAll(data)
}
Log.d(TAG, "Loaded ${peerIdIndex.size} peerID→npub mappings")
}
} catch (e: Exception) {
Expand All @@ -302,7 +292,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte

private fun savePeerIdIndex() {
try {
val json = gson.toJson(peerIdIndex)
val json = JsonUtil.toJson(MapSerializer(String.serializer(), String.serializer()), peerIdIndex)
stateManager.storeSecureValue(PEERID_INDEX_KEY, json)
Log.d(TAG, "Saved ${peerIdIndex.size} peerID→npub mappings")
} catch (e: Exception) {
Expand Down Expand Up @@ -336,6 +326,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
}

/** Serializable data for JSON storage */
@Serializable
private data class FavoriteRelationshipData(
val peerNoisePublicKeyHex: String,
val peerNostrPublicKey: String?,
Expand Down
Loading
Loading