Rebrand from Casera/MyCrib to honeyDue
Total rebrand across KMM project: - Kotlin package: com.example.casera -> com.tt.honeyDue (dirs + declarations) - Gradle: rootProject.name, namespace, applicationId - Android: manifest, strings.xml (all languages), widget resources - iOS: pbxproj bundle IDs, Info.plist, entitlements, xcconfig - iOS directories: Casera/ -> HoneyDue/, CaseraTests/ -> HoneyDueTests/, etc. - Swift source: all class/struct/enum renames - Deep links: casera:// -> honeydue://, .casera -> .honeydue - App icons replaced with honeyDue honeycomb icon - Domains: casera.treytartt.com -> honeyDue.treytartt.com - Bundle IDs: com.tt.casera -> com.tt.honeyDue - Database table names preserved Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
package com.tt.honeyDue
|
||||
|
||||
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.tt.honeyDue.storage.TokenManager
|
||||
import com.tt.honeyDue.storage.TokenStorage
|
||||
import com.tt.honeyDue.storage.TaskCacheManager
|
||||
import com.tt.honeyDue.storage.TaskCacheStorage
|
||||
import com.tt.honeyDue.storage.ThemeStorage
|
||||
import com.tt.honeyDue.storage.ThemeStorageManager
|
||||
import com.tt.honeyDue.ui.theme.ThemeManager
|
||||
import com.tt.honeyDue.fcm.FCMManager
|
||||
import com.tt.honeyDue.platform.BillingManager
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.sharing.ContractorSharingManager
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.data.PersistenceManager
|
||||
import com.tt.honeyDue.models.honeyDuePackageType
|
||||
import com.tt.honeyDue.models.detecthoneyDuePackageType
|
||||
import com.tt.honeyDue.analytics.PostHogAnalytics
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity(), SingletonImageLoader.Factory {
|
||||
private var deepLinkResetToken by mutableStateOf<String?>(null)
|
||||
private var navigateToTaskId by mutableStateOf<Int?>(null)
|
||||
private var pendingContractorImportUri by mutableStateOf<Uri?>(null)
|
||||
private var pendingResidenceImportUri by mutableStateOf<Uri?>(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)
|
||||
|
||||
// Initialize PostHog Analytics
|
||||
PostHogAnalytics.initialize(application, debug = true) // Set debug=false for release
|
||||
|
||||
// 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.tt.honeyDue.network.NotificationApi()
|
||||
val deviceId = android.provider.Settings.Secure.getString(
|
||||
contentResolver,
|
||||
android.provider.Settings.Secure.ANDROID_ID
|
||||
)
|
||||
val request = com.tt.honeyDue.models.DeviceRegistrationRequest(
|
||||
deviceId = deviceId,
|
||||
registrationId = fcmToken,
|
||||
platform = "android",
|
||||
name = android.os.Build.MODEL
|
||||
)
|
||||
|
||||
when (val result = notificationApi.registerDevice(authToken, request)) {
|
||||
is com.tt.honeyDue.network.ApiResult.Success -> {
|
||||
Log.d("MainActivity", "Device registered successfully: ${result.data}")
|
||||
}
|
||||
is com.tt.honeyDue.network.ApiResult.Error -> {
|
||||
Log.e("MainActivity", "Failed to register device: ${result.message}")
|
||||
}
|
||||
is com.tt.honeyDue.network.ApiResult.Loading,
|
||||
is com.tt.honeyDue.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<String>,
|
||||
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()
|
||||
|
||||
// Check if widget completed a task - refresh data if dirty
|
||||
if (TaskCacheStorage.areTasksDirty()) {
|
||||
Log.d("MainActivity", "🔄 Tasks marked dirty by widget, refreshing...")
|
||||
TaskCacheStorage.clearDirtyFlag()
|
||||
// Force refresh tasks from API
|
||||
APILayer.getTasks(forceRefresh = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
val isResetLink = data != null &&
|
||||
data.scheme == "honeydue" &&
|
||||
data.host == "reset-password"
|
||||
if (isResetLink) {
|
||||
// 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.ishoneyDueFile(applicationContext, uri)) {
|
||||
Log.d("MainActivity", "honeyDue 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 = detecthoneyDuePackageType(jsonString)
|
||||
Log.d("MainActivity", "Detected package type: $packageType")
|
||||
|
||||
when (packageType) {
|
||||
honeyDuePackageType.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)
|
||||
}
|
||||
Reference in New Issue
Block a user