package com.example.casera import android.content.Intent import android.net.Uri import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.lifecycleScope import coil3.ImageLoader import coil3.PlatformContext import coil3.SingletonImageLoader import coil3.network.ktor3.KtorNetworkFetcherFactory import coil3.disk.DiskCache import coil3.memory.MemoryCache import coil3.request.crossfade import coil3.util.DebugLogger import okio.FileSystem import com.example.casera.storage.TokenManager import com.example.casera.storage.TokenStorage import com.example.casera.storage.TaskCacheManager import com.example.casera.storage.TaskCacheStorage import com.example.casera.storage.ThemeStorage import com.example.casera.storage.ThemeStorageManager import com.example.casera.ui.theme.ThemeManager import com.example.casera.fcm.FCMManager import com.example.casera.platform.BillingManager import com.example.casera.network.APILayer import com.example.casera.sharing.ContractorSharingManager import com.example.casera.data.DataManager import com.example.casera.data.PersistenceManager import com.example.casera.models.CaseraPackageType import com.example.casera.models.detectCaseraPackageType import kotlinx.coroutines.launch class MainActivity : ComponentActivity(), SingletonImageLoader.Factory { private var deepLinkResetToken by mutableStateOf(null) private var navigateToTaskId by mutableStateOf(null) private var pendingContractorImportUri by mutableStateOf(null) private var pendingResidenceImportUri by mutableStateOf(null) private lateinit var billingManager: BillingManager override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) // Initialize TokenStorage with Android TokenManager TokenStorage.initialize(TokenManager.getInstance(applicationContext)) // Initialize TaskCacheStorage for offline task caching TaskCacheStorage.initialize(TaskCacheManager.getInstance(applicationContext)) // Initialize ThemeStorage and ThemeManager ThemeStorage.initialize(ThemeStorageManager.getInstance(applicationContext)) ThemeManager.initialize() // Initialize DataManager with platform-specific managers // This loads cached lookup data from disk for faster startup DataManager.initialize( tokenMgr = TokenManager.getInstance(applicationContext), themeMgr = ThemeStorageManager.getInstance(applicationContext), persistenceMgr = PersistenceManager.getInstance(applicationContext) ) // Initialize BillingManager for subscription management billingManager = BillingManager.getInstance(applicationContext) // Handle deep link, notification navigation, and file import from intent handleDeepLink(intent) handleNotificationNavigation(intent) handleFileImport(intent) // Request notification permission and setup FCM setupFCM() // Verify subscriptions if user is authenticated verifySubscriptionsOnLaunch() setContent { App( deepLinkResetToken = deepLinkResetToken, onClearDeepLinkToken = { deepLinkResetToken = null }, navigateToTaskId = navigateToTaskId, onClearNavigateToTask = { navigateToTaskId = null }, pendingContractorImportUri = pendingContractorImportUri, onClearContractorImport = { pendingContractorImportUri = null }, pendingResidenceImportUri = pendingResidenceImportUri, onClearResidenceImport = { pendingResidenceImportUri = null } ) } } /** * Verify subscriptions with Google Play and sync with backend on app launch */ private fun verifySubscriptionsOnLaunch() { val authToken = TokenStorage.getToken() if (authToken == null) { Log.d("MainActivity", "No auth token, skipping subscription verification") return } Log.d("MainActivity", "🔄 Verifying subscriptions on launch...") billingManager.startConnection( onSuccess = { Log.d("MainActivity", "✅ Billing connected, restoring purchases...") lifecycleScope.launch { val restored = billingManager.restorePurchases() if (restored) { Log.d("MainActivity", "✅ Subscriptions verified and synced with backend") } else { Log.d("MainActivity", "📦 No active subscriptions found") } } }, onError = { error -> Log.e("MainActivity", "❌ Failed to connect to billing: $error") } ) } private fun setupFCM() { // Request notification permission if needed if (!FCMManager.isNotificationPermissionGranted(this)) { FCMManager.requestNotificationPermission(this) } // Get FCM token and register with backend lifecycleScope.launch { val fcmToken = FCMManager.getFCMToken() if (fcmToken != null) { Log.d("MainActivity", "FCM Token: $fcmToken") registerDeviceWithBackend(fcmToken) } } } private suspend fun registerDeviceWithBackend(fcmToken: String) { try { val authToken = TokenStorage.getToken() if (authToken != null) { val notificationApi = com.example.casera.network.NotificationApi() val deviceId = android.provider.Settings.Secure.getString( contentResolver, android.provider.Settings.Secure.ANDROID_ID ) val request = com.example.casera.models.DeviceRegistrationRequest( deviceId = deviceId, registrationId = fcmToken, platform = "android", name = android.os.Build.MODEL ) when (val result = notificationApi.registerDevice(authToken, request)) { is com.example.casera.network.ApiResult.Success -> { Log.d("MainActivity", "Device registered successfully: ${result.data}") } is com.example.casera.network.ApiResult.Error -> { Log.e("MainActivity", "Failed to register device: ${result.message}") } is com.example.casera.network.ApiResult.Loading, is com.example.casera.network.ApiResult.Idle -> { // These states shouldn't occur for direct API calls } } } else { Log.d("MainActivity", "No auth token available, will register device after login") } } catch (e: Exception) { Log.e("MainActivity", "Error registering device", e) } } @Deprecated("Deprecated in Java") override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { FCMManager.NOTIFICATION_PERMISSION_REQUEST_CODE -> { if (grantResults.isNotEmpty() && grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED) { Log.d("MainActivity", "Notification permission granted") // Get FCM token now that permission is granted lifecycleScope.launch { FCMManager.getFCMToken() } } else { Log.d("MainActivity", "Notification permission denied") } } } } override fun onResume() { super.onResume() // Check if lookups have changed on server (efficient ETag-based check) // This ensures app has fresh data when coming back from background lifecycleScope.launch { Log.d("MainActivity", "🔄 App resumed, checking for lookup updates...") APILayer.refreshLookupsIfChanged() } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handleDeepLink(intent) handleNotificationNavigation(intent) handleFileImport(intent) } private fun handleNotificationNavigation(intent: Intent?) { val taskId = intent?.getIntExtra(NotificationActionReceiver.EXTRA_NAVIGATE_TO_TASK, -1) if (taskId != null && taskId != -1) { Log.d("MainActivity", "Navigating to task from notification: $taskId") navigateToTaskId = taskId } } private fun handleDeepLink(intent: Intent?) { val data: Uri? = intent?.data if (data != null && data.scheme == "mycrib" && data.host == "reset-password") { // Extract token from query parameter val token = data.getQueryParameter("token") if (token != null) { deepLinkResetToken = token println("Deep link received with token: $token") } } } private fun handleFileImport(intent: Intent?) { if (intent?.action == Intent.ACTION_VIEW) { val uri = intent.data if (uri != null && ContractorSharingManager.isCaseraFile(applicationContext, uri)) { Log.d("MainActivity", "Casera file received: $uri") // Read file content to detect package type try { val inputStream = contentResolver.openInputStream(uri) if (inputStream != null) { val jsonString = inputStream.bufferedReader().use { it.readText() } inputStream.close() val packageType = detectCaseraPackageType(jsonString) Log.d("MainActivity", "Detected package type: $packageType") when (packageType) { CaseraPackageType.RESIDENCE -> { Log.d("MainActivity", "Routing to residence import") pendingResidenceImportUri = uri } else -> { // Default to contractor for backward compatibility Log.d("MainActivity", "Routing to contractor import") pendingContractorImportUri = uri } } } } catch (e: Exception) { Log.e("MainActivity", "Failed to detect package type, defaulting to contractor", e) // Default to contractor on error for backward compatibility pendingContractorImportUri = uri } } } } override fun newImageLoader(context: PlatformContext): ImageLoader { return ImageLoader.Builder(context) .components { add(KtorNetworkFetcherFactory()) } .memoryCache { MemoryCache.Builder() .maxSizePercent(context, 0.25) .build() } .diskCache { DiskCache.Builder() .directory(FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "image_cache") .maxSizeBytes(512L * 1024 * 1024) // 512MB .build() } .crossfade(true) .logger(DebugLogger()) .build() } } @Preview @Composable fun AppAndroidPreview() { App(deepLinkResetToken = null) }