11package com.uc.caffeine
22
3+ import android.Manifest
4+ import android.content.pm.PackageManager
5+ import android.os.Build
36import android.os.Bundle
47import androidx.activity.ComponentActivity
58import androidx.activity.compose.setContent
69import androidx.activity.enableEdgeToEdge
10+ import androidx.activity.result.contract.ActivityResultContracts
711import androidx.compose.animation.AnimatedContent
812import androidx.compose.animation.AnimatedVisibility
913import androidx.compose.animation.SizeTransform
@@ -81,12 +85,16 @@ import androidx.compose.runtime.mutableIntStateOf
8185import androidx.compose.runtime.mutableStateMapOf
8286import androidx.compose.runtime.mutableStateOf
8387import androidx.compose.runtime.setValue
88+ import androidx.lifecycle.compose.LifecycleResumeEffect
89+ import androidx.lifecycle.lifecycleScope
90+ import kotlinx.coroutines.launch
8491import androidx.compose.ui.Alignment
8592import androidx.compose.ui.Modifier
8693import androidx.compose.ui.layout.boundsInParent
8794import androidx.compose.ui.layout.onGloballyPositioned
8895import androidx.compose.ui.platform.testTag
8996import androidx.compose.ui.res.painterResource
97+ import androidx.compose.ui.res.stringResource
9098import androidx.compose.ui.text.font.FontWeight
9199import androidx.compose.ui.tooling.preview.PreviewScreenSizes
92100import androidx.compose.ui.unit.dp
@@ -109,11 +117,27 @@ import com.uc.caffeine.ui.screens.settings.SettingsScreen
109117import com.uc.caffeine.ui.theme.CaffeineTheme
110118import com.uc.caffeine.ui.theme.MontserratFamily
111119import com.uc.caffeine.ui.viewmodel.CaffeineViewModel
120+ import com.uc.caffeine.widget.CaffeineWidgetUpdater
112121
113122class MainActivity : ComponentActivity () {
123+
124+ private val requestNotificationPermission = registerForActivityResult(
125+ ActivityResultContracts .RequestPermission (),
126+ ) { /* result handled by the system — no-op */ }
127+
114128 override fun onCreate (savedInstanceState : Bundle ? ) {
115129 super .onCreate(savedInstanceState)
116130 enableEdgeToEdge()
131+ com.uc.caffeine.util.notifications.NotificationChannels .createChannels(this )
132+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ) {
133+ if (checkSelfPermission(Manifest .permission.POST_NOTIFICATIONS ) != PackageManager .PERMISSION_GRANTED ) {
134+ requestNotificationPermission.launch(Manifest .permission.POST_NOTIFICATIONS )
135+ }
136+ }
137+ CaffeineWidgetUpdater .schedulePeriodicRefresh(this )
138+ lifecycleScope.launch {
139+ CaffeineWidgetUpdater .publishWidgetPreviews(applicationContext)
140+ }
117141 setContent {
118142 CaffeineApp ()
119143 }
@@ -137,6 +161,10 @@ fun CaffeineApp(
137161 val userSettings by viewModel.userSettings.collectAsStateWithLifecycle()
138162 val isUserSettingsLoaded by viewModel.isUserSettingsLoaded.collectAsStateWithLifecycle()
139163 val isConsumptionEntriesLoading by viewModel.isConsumptionEntriesLoading.collectAsStateWithLifecycle()
164+ LifecycleResumeEffect (Unit ) {
165+ viewModel.onAppOpened()
166+ onPauseOrDispose {}
167+ }
140168 val hasExistingConsumptionHistory by viewModel.hasExistingConsumptionHistory.collectAsStateWithLifecycle()
141169 val darkTheme = when (userSettings.themeMode) {
142170 ThemeMode .SYSTEM -> isSystemInDarkTheme()
@@ -153,40 +181,47 @@ fun CaffeineApp(
153181 darkTheme = darkTheme,
154182 dynamicColor = userSettings.useDynamicColor,
155183 ) {
156- AnimatedContent (
157- targetState = startupDestination,
184+ // Backdrop for the startup transition: the Onboarding→Main scaleIn from 0.92 leaves a
185+ // margin around the incoming shell, which would otherwise expose the windowBackground.
186+ Surface (
158187 modifier = Modifier .fillMaxSize(),
159- transitionSpec = {
160- when {
161- initialState == StartupDestination .Onboarding &&
162- targetState == StartupDestination .Main -> {
163- (
164- fadeIn(animationSpec = tween(durationMillis = 500 )) +
165- scaleIn(
166- initialScale = 0.92f ,
167- animationSpec = tween(durationMillis = 600 , easing = EaseOut ),
168- )
169- ) togetherWith (
170- fadeOut(animationSpec = tween(durationMillis = 400 )) +
171- scaleOut(
172- targetScale = 1.06f ,
173- animationSpec = tween(durationMillis = 400 ),
188+ color = MaterialTheme .colorScheme.background,
189+ ) {
190+ AnimatedContent (
191+ targetState = startupDestination,
192+ modifier = Modifier .fillMaxSize(),
193+ transitionSpec = {
194+ when {
195+ initialState == StartupDestination .Onboarding &&
196+ targetState == StartupDestination .Main -> {
197+ (
198+ fadeIn(animationSpec = tween(durationMillis = 500 )) +
199+ scaleIn(
200+ initialScale = 0.92f ,
201+ animationSpec = tween(durationMillis = 600 , easing = EaseOut ),
202+ )
203+ ) togetherWith (
204+ fadeOut(animationSpec = tween(durationMillis = 400 )) +
205+ scaleOut(
206+ targetScale = 1.06f ,
207+ animationSpec = tween(durationMillis = 400 ),
208+ )
174209 )
175- )
176- }
210+ }
177211
178- else -> {
179- fadeIn(animationSpec = tween(durationMillis = 220 , delayMillis = 40 )) togetherWith
180- fadeOut(animationSpec = tween(durationMillis = 180 ))
181- }
182- }.using(SizeTransform (clip = false ))
183- },
184- label = " startup_destination_transition" ,
185- ) { destination ->
186- when (destination) {
187- StartupDestination .Loading -> StartupLoadingScreen ()
188- StartupDestination .Onboarding -> OnboardingRoot (displaySettings = userSettings)
189- StartupDestination .Main -> MainAppShell (userSettings = userSettings)
212+ else -> {
213+ fadeIn(animationSpec = tween(durationMillis = 220 , delayMillis = 40 )) togetherWith
214+ fadeOut(animationSpec = tween(durationMillis = 180 ))
215+ }
216+ }.using(SizeTransform (clip = false ))
217+ },
218+ label = " startup_destination_transition" ,
219+ ) { destination ->
220+ when (destination) {
221+ StartupDestination .Loading -> StartupLoadingScreen ()
222+ StartupDestination .Onboarding -> OnboardingRoot (displaySettings = userSettings)
223+ StartupDestination .Main -> MainAppShell (userSettings = userSettings)
224+ }
190225 }
191226 }
192227 }
@@ -341,7 +376,7 @@ internal fun MainAppShell(
341376 ),
342377 ) {
343378 Text (
344- text = destination.label ,
379+ text = stringResource( destination.labelRes) ,
345380 modifier = Modifier .padding(start = ButtonDefaults .IconSpacing ),
346381 style = MaterialTheme .typography.titleSmall.copy(
347382 fontWeight = FontWeight .Bold ,
@@ -430,7 +465,7 @@ private fun DestinationIcon(
430465 if (vectorIcon != null ) {
431466 Icon (
432467 imageVector = vectorIcon,
433- contentDescription = destination.label ,
468+ contentDescription = stringResource( destination.labelRes) ,
434469 modifier = Modifier .size(24 .dp),
435470 )
436471 return
@@ -445,7 +480,7 @@ private fun DestinationIcon(
445480 if (iconRes != null ) {
446481 Icon (
447482 painter = painterResource(iconRes),
448- contentDescription = destination.label ,
483+ contentDescription = stringResource( destination.labelRes) ,
449484 modifier = Modifier .size(24 .dp),
450485 )
451486 }
@@ -489,7 +524,7 @@ private fun AddConsumptionButton(
489524 .background(Color .White ),
490525 )
491526 Text (
492- text = " Add Consumption " ,
527+ text = stringResource( R .string.main_add_consumption) ,
493528 style = MaterialTheme .typography.titleMedium.copy(
494529 fontFamily = MontserratFamily ,
495530 fontWeight = FontWeight .Bold ,
0 commit comments