diff --git a/app/src/main/java/com/bitchat/android/BitchatApplication.kt b/app/src/main/java/com/bitchat/android/BitchatApplication.kt index 7b5926ef..b4107ba3 100644 --- a/app/src/main/java/com/bitchat/android/BitchatApplication.kt +++ b/app/src/main/java/com/bitchat/android/BitchatApplication.kt @@ -4,10 +4,12 @@ import android.app.Application import com.bitchat.android.nostr.RelayDirectory import com.bitchat.android.ui.theme.ThemePreferenceManager import com.bitchat.android.net.ArtiTorManager +import dagger.hilt.android.HiltAndroidApp /** * Main application class for bitchat Android */ +@HiltAndroidApp class BitchatApplication : Application() { override fun onCreate() { diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 3d752291..41d95a1e 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.Lifecycle import com.bitchat.android.mesh.BluetoothMeshService @@ -46,9 +45,11 @@ import com.bitchat.android.ui.theme.BitchatTheme import com.bitchat.android.wifiaware.WifiAwareController import com.bitchat.android.nostr.PoWPreferenceManager import com.bitchat.android.services.VerificationService +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch +@AndroidEntryPoint class MainActivity : OrientationAwareActivity() { private lateinit var permissionManager: PermissionManager @@ -62,15 +63,8 @@ class MainActivity : OrientationAwareActivity() { private lateinit var unifiedMeshService: MeshService private val mainViewModel: MainViewModel by viewModels() private var pendingMeshForegroundServiceStart = false - private val chatViewModel: ChatViewModel by viewModels { - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return ChatViewModel(application, meshService, unifiedMeshService) as T - } - } - } - + private val chatViewModel: ChatViewModel by viewModels() + private val forceFinishReceiver = object : android.content.BroadcastReceiver() { override fun onReceive(context: android.content.Context, intent: android.content.Intent) { if (intent.action == com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH) { diff --git a/app/src/main/java/com/bitchat/android/MainViewModel.kt b/app/src/main/java/com/bitchat/android/MainViewModel.kt index 15ec6fda..bd55ce02 100644 --- a/app/src/main/java/com/bitchat/android/MainViewModel.kt +++ b/app/src/main/java/com/bitchat/android/MainViewModel.kt @@ -7,9 +7,12 @@ import com.bitchat.android.onboarding.OnboardingState import com.bitchat.android.onboarding.BatteryOptimizationStatus import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject -class MainViewModel : ViewModel() { +@HiltViewModel +class MainViewModel @Inject constructor() : ViewModel() { private val _onboardingState = MutableStateFlow(OnboardingState.CHECKING) val onboardingState: StateFlow = _onboardingState.asStateFlow() diff --git a/app/src/main/java/com/bitchat/android/di/MeshModule.kt b/app/src/main/java/com/bitchat/android/di/MeshModule.kt new file mode 100644 index 00000000..2cb8453c --- /dev/null +++ b/app/src/main/java/com/bitchat/android/di/MeshModule.kt @@ -0,0 +1,36 @@ +package com.bitchat.android.di + +import android.content.Context +import com.bitchat.android.mesh.BluetoothMeshService +import com.bitchat.android.mesh.MeshService +import com.bitchat.android.service.MeshServiceHolder +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +/** + * Bridges the process-wide mesh instances into the injection graph. + * + * [MeshServiceHolder] remains the single source of truth: it is shared with the + * foreground service, it synchronises creation, and it supports replacing the + * mesh service after a panic clear. These bindings are therefore deliberately + * unscoped — each injection re-reads the holder. Caching them with @Singleton + * would pin the first instance for the process lifetime and hand out a stale + * mesh service after a replacement. + */ +@Module +@InstallIn(SingletonComponent::class) +object MeshModule { + + @Provides + fun provideBluetoothMeshService( + @ApplicationContext context: Context + ): BluetoothMeshService = MeshServiceHolder.getOrCreate(context) + + @Provides + fun provideMeshService( + @ApplicationContext context: Context + ): MeshService = MeshServiceHolder.getUnifiedOrCreate(context) +} diff --git a/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt b/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt index 35d0f4ec..8f894f2d 100644 --- a/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt +++ b/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt @@ -7,6 +7,7 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels +import dagger.hilt.android.AndroidEntryPoint import androidx.compose.animation.Crossfade import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -47,6 +48,7 @@ import java.io.File * Activity for managing Wi-Fi P2P hotspot for offline APK sharing. * Pure Compose implementation, no fragments. */ +@AndroidEntryPoint class HotspotActivity : ComponentActivity() { companion object { diff --git a/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt b/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt index 73f9faa4..d012ccbb 100644 --- a/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt +++ b/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt @@ -7,6 +7,8 @@ import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.bitchat.android.wifiaware.WifiAwareController +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -16,7 +18,10 @@ import java.io.File /** * ViewModel for managing hotspot state and lifecycle. */ -class HotspotViewModel(application: Application) : AndroidViewModel(application) { +@HiltViewModel +class HotspotViewModel @Inject constructor( + application: Application +) : AndroidViewModel(application) { companion object { private const val TAG = "HotspotViewModel" diff --git a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt index 54ab0318..0fa6bd72 100644 --- a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt @@ -63,8 +63,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.bitchat.android.ui.theme.BitchatFontFamily import com.bitchat.android.R import com.bitchat.android.core.ui.component.button.CloseButton @@ -617,7 +617,7 @@ fun AboutSheet( ) // === Prepare App for Sharing Section === - val apkViewModel: ApkDownloadViewModel = viewModel() + val apkViewModel: ApkDownloadViewModel = hiltViewModel() val apkUiState by apkViewModel.state.collectAsStateWithLifecycle() val apkStatus = apkUiState.apkStatus val releaseStatus = apkUiState.releaseStatus diff --git a/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt index 4d0c2d5b..dcc6d4af 100644 --- a/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt @@ -15,6 +15,8 @@ import com.bitchat.android.util.LatestReleaseProvider import com.bitchat.android.util.ShareableApkVariant import com.bitchat.android.util.UniversalApkManager import com.bitchat.android.util.WorkManagerApkDownloader +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel @@ -151,6 +153,7 @@ sealed class ApkUiEffect { * ViewModel for APK download/status/share logic following MVI pattern. * UI sends [ApkUiEvent], observes [ApkUiState], and collects [ApkUiEffect]. */ +@HiltViewModel class ApkDownloadViewModel internal constructor( application: Application, private val apkManager: UniversalApkManager, @@ -158,6 +161,11 @@ class ApkDownloadViewModel internal constructor( private val latestReleaseProvider: LatestReleaseProvider ) : AndroidViewModel(application) { + // Hilt injects through this constructor rather than the primary one: + // UniversalApkManager's collaborators are internal, and routing internal + // Kotlin types through Dagger's generated Java buys nothing here. Tests + // keep using the primary constructor to substitute fakes. + @Inject constructor(application: Application) : this( application = application, apkManager = UniversalApkManager(application), 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 76e784bc..ed1b4ec3 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -7,6 +7,8 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.bitchat.android.favorites.FavoritesChangeListener import com.bitchat.android.favorites.FavoritesPersistenceService +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -53,7 +55,8 @@ private data class ConversationLiveIdentityState( * Refactored ChatViewModel - Main coordinator for bitchat functionality * Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility */ -class ChatViewModel( +@HiltViewModel +class ChatViewModel @Inject constructor( application: Application, initialMeshService: BluetoothMeshService, initialUnifiedMeshService: MeshService