From 1b5113a835d1b25102366e26e433ac79ae9c0852 Mon Sep 17 00:00:00 2001 From: Ghosty <105041921+ghostyapps@users.noreply.github.com> Date: Tue, 11 Nov 2025 06:18:39 +0300 Subject: [PATCH] Add battery status screen --- app/build.gradle.kts | 1 + .../heyreminder/BatteryStatusViewModel.kt | 99 ++++++++ .../com/dcapps/heyreminder/MainActivity.kt | 230 +++++++++++++----- .../com/dcapps/heyreminder/ui/theme/Color.kt | 19 ++ .../com/dcapps/heyreminder/ui/theme/Theme.kt | 99 +++----- .../com/dcapps/heyreminder/ui/theme/Type.kt | 5 + 6 files changed, 331 insertions(+), 122 deletions(-) create mode 100644 app/src/main/java/com/dcapps/heyreminder/BatteryStatusViewModel.kt create mode 100644 app/src/main/java/com/dcapps/heyreminder/ui/theme/Color.kt create mode 100644 app/src/main/java/com/dcapps/heyreminder/ui/theme/Type.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 077c136..219d7c1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.code.gson:gson:2.10.1") implementation("androidx.compose.material:material:1.5.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") } diff --git a/app/src/main/java/com/dcapps/heyreminder/BatteryStatusViewModel.kt b/app/src/main/java/com/dcapps/heyreminder/BatteryStatusViewModel.kt new file mode 100644 index 0000000..204d586 --- /dev/null +++ b/app/src/main/java/com/dcapps/heyreminder/BatteryStatusViewModel.kt @@ -0,0 +1,99 @@ +package com.dcapps.heyreminder + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import androidx.core.content.ContextCompat +import androidx.lifecycle.AndroidViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class BatteryStatusViewModel(application: Application) : AndroidViewModel(application) { + + private val appContext = application.applicationContext + private val preferences = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + private val _uiState = MutableStateFlow(BatteryUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val batteryReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent == null) return + updateBatteryState(intent) + } + } + + init { + val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + val stickyIntent = ContextCompat.registerReceiver( + appContext, + batteryReceiver, + filter, + ContextCompat.RECEIVER_NOT_EXPORTED + ) + if (stickyIntent != null) { + updateBatteryState(stickyIntent) + } + } + + override fun onCleared() { + runCatching { appContext.unregisterReceiver(batteryReceiver) } + super.onCleared() + } + + private fun updateBatteryState(intent: Intent) { + val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + + if (level < 0 || scale <= 0) return + + val percentage = (level * 100) / scale + val isFull = status == BatteryManager.BATTERY_STATUS_FULL || percentage >= 100 + + if (isFull) { + saveLastFullChargeTime(System.currentTimeMillis()) + } + + val lastFullCharge = preferences.getLong(KEY_LAST_FULL_CHARGE, -1L).takeIf { it > 0L } + val estimatedRemaining = calculateEstimatedRemainingTime(lastFullCharge, percentage) + + _uiState.value = BatteryUiState( + batteryPercentage = percentage, + lastFullChargeTimestamp = lastFullCharge, + estimatedRemainingTimeMillis = estimatedRemaining + ) + } + + private fun saveLastFullChargeTime(timestamp: Long) { + preferences.edit().putLong(KEY_LAST_FULL_CHARGE, timestamp).apply() + } + + private fun calculateEstimatedRemainingTime(lastFullCharge: Long?, percentage: Int): Long? { + if (lastFullCharge == null) return null + if (percentage <= 0 || percentage >= 100) return null + + val elapsed = System.currentTimeMillis() - lastFullCharge + if (elapsed <= 0) return null + + val consumedPercent = 100 - percentage + if (consumedPercent <= 0) return null + + return (elapsed * percentage) / consumedPercent + } + + companion object { + private const val PREFS_NAME = "battery_prefs" + private const val KEY_LAST_FULL_CHARGE = "last_full_charge" + } +} + +data class BatteryUiState( + val batteryPercentage: Int = 0, + val lastFullChargeTimestamp: Long? = null, + val estimatedRemainingTimeMillis: Long? = null +) diff --git a/app/src/main/java/com/dcapps/heyreminder/MainActivity.kt b/app/src/main/java/com/dcapps/heyreminder/MainActivity.kt index 90c9a95..bb4dffd 100644 --- a/app/src/main/java/com/dcapps/heyreminder/MainActivity.kt +++ b/app/src/main/java/com/dcapps/heyreminder/MainActivity.kt @@ -1,79 +1,195 @@ package com.dcapps.heyreminder -import com.dcapps.heyreminder.data.ReminderScheduler -import android.app.AlarmManager -import android.content.Context -import android.content.Intent -import android.provider.Settings import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import com.dcapps.heyreminder.ui.MainScreen -import android.app.NotificationChannel -import android.app.NotificationManager -import android.os.Build +import androidx.activity.viewModels +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import com.dcapps.heyreminder.ui.theme.ReminderAppTheme +import kotlinx.coroutines.delay +import java.text.DateFormat +import java.util.Date -import android.Manifest -import android.content.pm.PackageManager -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat +class MainActivity : ComponentActivity() { + private val viewModel: BatteryStatusViewModel by viewModels() -import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowInsetsControllerCompat - -import androidx.appcompat.app.AppCompatDelegate -import androidx.appcompat.app.AppCompatActivity -import com.dcapps.heyreminder.data.ReminderRepository + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + ReminderAppTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + BatteryStatusScreen(viewModel = viewModel) + } + } + } + } +} -import android.content.res.Configuration +@Composable +private fun BatteryStatusScreen(viewModel: BatteryStatusViewModel) { + val uiState by viewModel.uiState.collectAsState() + var currentTimeMillis by remember { mutableLongStateOf(System.currentTimeMillis()) } + LaunchedEffect(Unit) { + while (true) { + delay(60_000) + currentTimeMillis = System.currentTimeMillis() + } + } -import android.graphics.Color + val timeSinceFullCharge = uiState.lastFullChargeTimestamp?.let { lastFull -> + if (lastFull <= currentTimeMillis) currentTimeMillis - lastFull else null + } + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 16.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + BatteryPercentageCard(percentage = uiState.batteryPercentage) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + InfoCard( + modifier = Modifier.weight(1f), + title = "Last full charge", + value = uiState.lastFullChargeTimestamp?.let { formatTimestamp(it) } ?: "Unknown" + ) + + InfoCard( + modifier = Modifier.weight(1f), + title = "Time since full", + value = timeSinceFullCharge?.let { formatDuration(it) } ?: "Unknown" + ) + } -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - installSplashScreen() + InfoCard( + modifier = Modifier.fillMaxWidth(), + title = "Estimated remaining", + value = uiState.estimatedRemainingTimeMillis?.let { formatDuration(it) } ?: "Unavailable" + ) + } +} - super.onCreate(savedInstanceState) - // Make system bars match our light-gray background - window.statusBarColor = ContextCompat.getColor(this, R.color.accent_color) - window.navigationBarColor = ContextCompat.getColor(this, R.color.white_background) - val isDarkTheme = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES - WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = !isDarkTheme - - // (1) Exact alarm izni isteme kodu… - - // (2) Bildirim kanalı oluşturma… - - // (3) Android 13+ bildirim izni iste - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) - != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions( - this, - arrayOf(Manifest.permission.POST_NOTIFICATIONS), - 1001 +@Composable +private fun BatteryPercentageCard(percentage: Int) { + Card( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Battery", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.BottomStart + ) { + Text( + text = "$percentage%", + style = MaterialTheme.typography.displayLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimaryContainer ) } } + } +} +@Composable +private fun InfoCard( + modifier: Modifier = Modifier, + title: String, + value: String +) { + Card( + modifier = modifier + .height(140.dp), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + verticalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + Text( + text = value, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSecondaryContainer, + textAlign = TextAlign.Start + ) + } + } +} - // SharedPreferences tabanlı hatırlatıcı deposunu başlat - ReminderRepository.init(applicationContext) +private fun formatTimestamp(timestamp: Long): String { + val formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT) + return formatter.format(Date(timestamp)) +} - // Reschedule all reminders on app start - val context = applicationContext - val reminders = ReminderRepository.getAllSync() - for (reminder in reminders) { - ReminderScheduler.schedule(context, reminder) - } +private fun formatDuration(durationMillis: Long): String { + if (durationMillis <= 0) return "Just now" - setContent { - ReminderAppTheme { - MainScreen() - } - } - } + val totalMinutes = durationMillis / 60000 + val days = totalMinutes / (60 * 24) + val hours = (totalMinutes / 60) % 24 + val minutes = totalMinutes % 60 + + val parts = mutableListOf() + if (days > 0) parts += "${days}d" + if (hours > 0) parts += "${hours}h" + if (minutes > 0) parts += "${minutes}m" + + return parts.joinToString(" ").ifEmpty { "<1m" } } diff --git a/app/src/main/java/com/dcapps/heyreminder/ui/theme/Color.kt b/app/src/main/java/com/dcapps/heyreminder/ui/theme/Color.kt new file mode 100644 index 0000000..192ae16 --- /dev/null +++ b/app/src/main/java/com/dcapps/heyreminder/ui/theme/Color.kt @@ -0,0 +1,19 @@ +package com.dcapps.heyreminder.ui.theme + +import androidx.compose.ui.graphics.Color + +val GreenPrimary = Color(0xFF388E3C) +val GreenPrimaryContainer = Color(0xFFC8E6C9) +val GreenOnPrimaryContainer = Color(0xFF0A3011) +val NeutralSurface = Color(0xFFF6F6F6) +val NeutralOnSurface = Color(0xFF1C1C1C) +val SecondaryContainer = Color(0xFFE8F5E9) +val OnSecondaryContainer = Color(0xFF1B421E) + +val DarkPrimary = Color(0xFF81C784) +val DarkPrimaryContainer = Color(0xFF2E7D32) +val DarkOnPrimaryContainer = Color(0xFFE8F5E9) +val DarkSurface = Color(0xFF121212) +val DarkOnSurface = Color(0xFFE1E1E1) +val DarkSecondaryContainer = Color(0xFF1B5E20) +val DarkOnSecondaryContainer = Color(0xFFC8E6C9) diff --git a/app/src/main/java/com/dcapps/heyreminder/ui/theme/Theme.kt b/app/src/main/java/com/dcapps/heyreminder/ui/theme/Theme.kt index 7ef5136..d7cd309 100644 --- a/app/src/main/java/com/dcapps/heyreminder/ui/theme/Theme.kt +++ b/app/src/main/java/com/dcapps/heyreminder/ui/theme/Theme.kt @@ -1,79 +1,48 @@ package com.dcapps.heyreminder.ui.theme +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.Typography -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp -import com.dcapps.heyreminder.R -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color -// Define your custom fonts in res/font (e.g., my_custom_font_regular.ttf, my_custom_font_medium.ttf, my_custom_font_bold.ttf) -private val CustomFontFamily = FontFamily( - Font(R.font.productsans_medium, FontWeight.Normal), - Font(R.font.productsans_bold, FontWeight.Medium), - Font(R.font.productsans_black, FontWeight.Bold) +private val LightColors = lightColorScheme( + primary = GreenPrimary, + onPrimary = Color.White, + primaryContainer = GreenPrimaryContainer, + onPrimaryContainer = GreenOnPrimaryContainer, + secondaryContainer = SecondaryContainer, + onSecondaryContainer = OnSecondaryContainer, + surface = NeutralSurface, + onSurface = NeutralOnSurface, + background = NeutralSurface, + onBackground = NeutralOnSurface ) -// Define typography using your custom font -private val AppTypography = Typography( - displayLarge = TextStyle( - fontFamily = CustomFontFamily, - fontWeight = FontWeight.Bold, - fontSize = 57.sp - ), - titleLarge = TextStyle( - fontFamily = CustomFontFamily, - fontWeight = FontWeight.Medium, - fontSize = 22.sp - ), - bodyLarge = TextStyle( - fontFamily = CustomFontFamily, - fontWeight = FontWeight.Normal, - fontSize = 16.sp - ), - labelLarge = TextStyle( - fontFamily = CustomFontFamily, - fontWeight = FontWeight.Medium, - fontSize = 14.sp - ) +private val DarkColors = darkColorScheme( + primary = DarkPrimary, + onPrimary = Color.Black, + primaryContainer = DarkPrimaryContainer, + onPrimaryContainer = DarkOnPrimaryContainer, + secondaryContainer = DarkSecondaryContainer, + onSecondaryContainer = DarkOnSecondaryContainer, + surface = DarkSurface, + onSurface = DarkOnSurface, + background = DarkSurface, + onBackground = DarkOnSurface ) @Composable -fun ReminderAppTheme(content: @Composable () -> Unit) { - // Load colors from resources - val primary = colorResource(R.color.accent_color) - val onPrimary = colorResource(R.color.text_color) - val background = colorResource(R.color.white_background) - val onBackground = colorResource(R.color.text_color) - val surface = background - val onSurface = onBackground - val darkColors = darkColorScheme( - primary = primary, - onPrimary = onPrimary, - background = background, - onBackground = onBackground, - surface = surface, - onSurface = onSurface - ) - val lightColors = lightColorScheme( - primary = primary, - onPrimary = onPrimary, - background = background, - onBackground = onBackground, - surface = surface, - onSurface = onSurface - ) - val colors = if (isSystemInDarkTheme()) darkColors else lightColors +fun ReminderAppTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = if (useDarkTheme) DarkColors else LightColors + MaterialTheme( - colorScheme = colors, - typography = AppTypography, + colorScheme = colorScheme, + typography = Typography, content = content ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/dcapps/heyreminder/ui/theme/Type.kt b/app/src/main/java/com/dcapps/heyreminder/ui/theme/Type.kt new file mode 100644 index 0000000..40f830c --- /dev/null +++ b/app/src/main/java/com/dcapps/heyreminder/ui/theme/Type.kt @@ -0,0 +1,5 @@ +package com.dcapps.heyreminder.ui.theme + +import androidx.compose.material3.Typography + +val Typography = Typography()