diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index fc8303f95a4e6eddc77d030af2844e2440e1493a..b268ef36cd2de3a14ddddf25aa9f5d4e95731e18 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -5,9 +5,6 @@ <SelectionState runConfigName="app"> <option name="selectionMode" value="DROPDOWN" /> </SelectionState> - <SelectionState runConfigName="MainActivity"> - <option name="selectionMode" value="DROPDOWN" /> - </SelectionState> </selectionStates> </component> </project> \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 002785e420efd8127f987ef64c4fc1f0b31cd507..63664ae28fff824ba65191fbb750adf06a11dac7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + + <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.VIBRATE"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> + <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" @@ -12,16 +17,20 @@ android:supportsRtl="true" android:theme="@style/Theme.Timer2" tools:targetApi="31"> + <activity android:name=".MainActivity" android:exported="true" android:theme="@style/Theme.Timer2"> <intent-filter> <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> + + <service + android:name="com.example.timer2.PomodoroTimerService" + android:foregroundServiceType="dataSync"/> </application> </manifest> \ No newline at end of file diff --git a/app/src/main/java/com/example/timer2/PomodoroTimerService.kt b/app/src/main/java/com/example/timer2/PomodoroTimerService.kt new file mode 100644 index 0000000000000000000000000000000000000000..6dba18e54647952bdd607576e0441c22b64a6f8f --- /dev/null +++ b/app/src/main/java/com/example/timer2/PomodoroTimerService.kt @@ -0,0 +1,97 @@ +package com.example.timer2 + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class PomodoroTimerService : Service() { + private var timerJob: Job? = null + private lateinit var notificationManager: NotificationManager + private val CHANNEL_ID = "PomodoroTimerChannel" + private val NOTIFICATION_ID = 1 + + override fun onCreate() { + super.onCreate() + notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + createNotificationChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + "START" -> { + val duration = intent.getLongExtra("DURATION", 0L) + startForegroundService(duration) + } + "STOP" -> stopSelf() + } + return START_NOT_STICKY + } + + private fun startForegroundService(duration: Long) { + val notification = createNotification("Pomodoro Timer Running", "Time left: ${formatTime(duration)}") + startForeground(NOTIFICATION_ID, notification) + + timerJob = CoroutineScope(Dispatchers.Default).launch { + var timeLeft = duration + while (timeLeft > 0) { + delay(1000) + timeLeft -= 1000 + updateNotification("Time left: ${formatTime(timeLeft)}") + } + stopSelf() + } + } + + private fun createNotification(title: String, content: String): Notification { + val intent = Intent(this, MainActivity::class.java) + val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(title) + .setContentText(content) + .setSmallIcon(R.drawable.ic_notification) // Make sure to add this icon to your drawable resources + .setContentIntent(pendingIntent) + .build() + } + + private fun updateNotification(content: String) { + val notification = createNotification("Pomodoro Timer Running", content) + notificationManager.notify(NOTIFICATION_ID, notification) + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + "Pomodoro Timer Channel", + NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel(channel) + } + } + + private fun formatTime(timeInMillis: Long): String { + val minutes = timeInMillis / 60000 + val seconds = (timeInMillis % 60000) / 1000 + return String.format("%02d:%02d", minutes, seconds) + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onDestroy() { + super.onDestroy() + timerJob?.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/timer2/PreferencesHelper.kt b/app/src/main/java/com/example/timer2/PreferencesHelper.kt index 9bad63345a8840e7fdf46602fb23de9391780750..db5d6df4307e62d48dca7c8972bc87e43e0bc342 100644 --- a/app/src/main/java/com/example/timer2/PreferencesHelper.kt +++ b/app/src/main/java/com/example/timer2/PreferencesHelper.kt @@ -5,16 +5,12 @@ import android.content.SharedPreferences class PreferencesHelper(context: Context) { private val prefs: SharedPreferences = context.getSharedPreferences("PomodoroPrefs", Context.MODE_PRIVATE) - fun getPomodoroDuration(): Int = prefs.getInt("pomodoro_duration", 25 * 60) // Default 25 minutes fun savePomodoroDuration(duration: Int) = prefs.edit().putInt("pomodoro_duration", duration).apply() - fun getDarkThemeEnabled(): Boolean = prefs.getBoolean("dark_theme_enabled", false) fun saveDarkThemeEnabled(enabled: Boolean) = prefs.edit().putBoolean("dark_theme_enabled", enabled).apply() - fun getSoundEnabled(): Boolean = prefs.getBoolean("sound_enabled", true) fun saveSoundEnabled(enabled: Boolean) = prefs.edit().putBoolean("sound_enabled", enabled).apply() - fun getVibrationEnabled(): Boolean = prefs.getBoolean("vibration_enabled", true) fun saveVibrationEnabled(enabled: Boolean) = prefs.edit().putBoolean("vibration_enabled", enabled).apply() -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/timer2/ui/theme/AppBarWithSettings.kt b/app/src/main/java/com/example/timer2/ui/theme/AppBarWithSettings.kt index 9e8367f5e462d41dbb6211a2bbc6eff389b0d4f6..b7f7fdd19e04da65e627e7499623351e0c3a88f9 100644 --- a/app/src/main/java/com/example/timer2/ui/theme/AppBarWithSettings.kt +++ b/app/src/main/java/com/example/timer2/ui/theme/AppBarWithSettings.kt @@ -10,15 +10,3 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.navigation.NavController -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AppBarWithSettings(navController: NavController) { - TopAppBar( - title = { Text("To-Do") }, - actions = { - IconButton(onClick = { navController.navigate("settings") }) { - Icon(Icons.Default.Settings, contentDescription = "Settings") - } - } - ) -} diff --git a/app/src/main/java/com/example/timer2/ui/theme/AppNavigation.kt b/app/src/main/java/com/example/timer2/ui/theme/AppNavigation.kt index 38caf9508572ae4a4f9277bc4b40f5f44e26dd24..f981c34ef01268cc977b20eb8229c76526dcc633 100644 --- a/app/src/main/java/com/example/timer2/ui/theme/AppNavigation.kt +++ b/app/src/main/java/com/example/timer2/ui/theme/AppNavigation.kt @@ -11,7 +11,6 @@ import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.example.timer2.PreferencesHelper import com.example.timer2.data.ToDoRepository -import com.example.timer2.viewmodel.SettingsViewModel import com.example.timer2.viewmodel.ToDoViewModel import com.example.timer2.viewmodel.ToDoViewModelFactory @@ -73,7 +72,7 @@ fun AppNavigation( if (toDoItem != null) { PomodoroTimerScreen( navController = navController, - timerDuration = toDoItem.duration , // Change for testing !!!!!!!!!! + timerDuration = toDoItem.duration * 60 , // Change for testing !!!!!!!!!! onTimerReset = { /* Logic for resetting the timer */ }, preferencesHelper = preferencesHelper, selectedTimerName = toDoItem.name diff --git a/app/src/main/java/com/example/timer2/ui/theme/Color.kt b/app/src/main/java/com/example/timer2/ui/theme/Color.kt index af50970c7fe7a8a70c991d781a730532b96fa499..179148f1be189ed9d7fae970eb17d28ce2ab2be3 100644 --- a/app/src/main/java/com/example/timer2/ui/theme/Color.kt +++ b/app/src/main/java/com/example/timer2/ui/theme/Color.kt @@ -1,4 +1,2 @@ package com.example.timer2.ui.theme -import androidx.compose.ui.graphics.Color - diff --git a/app/src/main/java/com/example/timer2/ui/theme/PomodoroTimerScreen.kt b/app/src/main/java/com/example/timer2/ui/theme/PomodoroTimerScreen.kt index 8d9e82014407eb1ff2e7aafce3ce5e38d3b2f4c4..eaed38e8e03e756a86a18ea008cab1bcb957001e 100644 --- a/app/src/main/java/com/example/timer2/ui/theme/PomodoroTimerScreen.kt +++ b/app/src/main/java/com/example/timer2/ui/theme/PomodoroTimerScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -53,6 +54,12 @@ fun PomodoroTimerScreen( viewModel.setInitialDuration(timerDuration) } + DisposableEffect(Unit) { + onDispose { + viewModel.resetTimer() // This will stop the foreground service when leaving the screen + } + } + Scaffold( // ... (keep existing top bar) ) { paddingValues -> diff --git a/app/src/main/java/com/example/timer2/ui/theme/Shapes.kt b/app/src/main/java/com/example/timer2/ui/theme/Shapes.kt index a492d8709108562251e0b2c50dc36d6ab3156c1b..b8fe2769a6bfa309dcd0fac4df3f512bdf59608f 100644 --- a/app/src/main/java/com/example/timer2/ui/theme/Shapes.kt +++ b/app/src/main/java/com/example/timer2/ui/theme/Shapes.kt @@ -4,8 +4,3 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp -val Shapes = Shapes( - small = RoundedCornerShape(4.dp), - medium = RoundedCornerShape(8.dp), - large = RoundedCornerShape(16.dp) -) diff --git a/app/src/main/java/com/example/timer2/viewmodel/PomodoroTimerViewModel.kt b/app/src/main/java/com/example/timer2/viewmodel/PomodoroTimerViewModel.kt index aa6136b2daa4f73b3b9c9de557606e67912b5e83..65749805491b11f8ebb53fffda9fff672420e341 100644 --- a/app/src/main/java/com/example/timer2/viewmodel/PomodoroTimerViewModel.kt +++ b/app/src/main/java/com/example/timer2/viewmodel/PomodoroTimerViewModel.kt @@ -1,7 +1,9 @@ package com.example.timer2.viewmodel import android.app.Application +import android.content.Intent import android.media.MediaPlayer +import android.os.Build import android.os.VibrationEffect import android.os.Vibrator import android.util.Log @@ -10,6 +12,7 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.example.timer2.PomodoroTimerService import com.example.timer2.PreferencesHelper import com.example.timer2.R import kotlinx.coroutines.Job @@ -31,7 +34,7 @@ enum class TimerState { } class PomodoroTimerViewModel( - application: Application, + private val application: Application, private val preferencesHelper: PreferencesHelper ) : AndroidViewModel(application) { @@ -90,6 +93,8 @@ class PomodoroTimerViewModel( if (_timerState.value == TimerState.RUNNING) return _timerState.value = TimerState.RUNNING + startForegroundService(duration) + timerJob = viewModelScope.launch { val endTime = System.currentTimeMillis() + duration while (System.currentTimeMillis() < endTime && _timerState.value == TimerState.RUNNING) { @@ -100,6 +105,25 @@ class PomodoroTimerViewModel( } } + private fun startForegroundService(duration: Long) { + val intent = Intent(application, PomodoroTimerService::class.java).apply { + action = "START" + putExtra("DURATION", duration) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + application.startForegroundService(intent) + } else { + application.startService(intent) + } + } + + private fun stopForegroundService() { + val intent = Intent(application, PomodoroTimerService::class.java).apply { + action = "STOP" + } + application.stopService(intent) + } + fun selectTimer(timerType: TimerType) { _timerType.value = timerType _currentTimerDuration.value = when (timerType) { @@ -144,6 +168,7 @@ class PomodoroTimerViewModel( private fun pauseTimer() { _timerState.value = TimerState.PAUSED timerJob?.cancel() + stopForegroundService() } fun resetTimer() { @@ -152,6 +177,7 @@ class PomodoroTimerViewModel( _timerType.value = TimerType.NORMAL _currentTimerDuration.value = initialDuration _timeLeft.value = initialDuration + stopForegroundService() } override fun onCleared() { diff --git a/app/src/main/java/com/example/timer2/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/example/timer2/viewmodel/SettingsViewModel.kt deleted file mode 100644 index ade80832b6f194c8815729c630225f7060fefe58..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/timer2/viewmodel/SettingsViewModel.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.timer2.viewmodel - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel - -class SettingsViewModel : ViewModel() { - private val _pomodoroTime = MutableLiveData<Int>(25 * 60) // Default 25 minutes in seconds - val pomodoroTime: LiveData<Int> = _pomodoroTime - - private val _isDarkMode = MutableLiveData<Boolean>(false) - val isDarkMode: LiveData<Boolean> = _isDarkMode - - private val _isSoundEnabled = MutableLiveData<Boolean>(true) - val isSoundEnabled: LiveData<Boolean> = _isSoundEnabled - - private val _isVibrationEnabled = MutableLiveData<Boolean>(true) - val isVibrationEnabled: LiveData<Boolean> = _isVibrationEnabled - - private val _showInputDialog = MutableLiveData<Boolean>(false) - val showInputDialog: LiveData<Boolean> = _showInputDialog - - fun setPomodoroTime(minutes: Int) { - _pomodoroTime.value = minutes * 60 // Convert minutes to seconds - } - - fun setDarkMode(isDark: Boolean) { - _isDarkMode.value = isDark - } - - fun setSoundEnabled(isEnabled: Boolean) { - _isSoundEnabled.value = isEnabled - } - - fun setVibrationEnabled(isEnabled: Boolean) { - _isVibrationEnabled.value = isEnabled - } - - fun toggleInputDialog(show: Boolean) { - _showInputDialog.value = show - } -} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 0000000000000000000000000000000000000000..a8b409b1d1a67ab9d6be93a1920501644837a571 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + +</selector> \ No newline at end of file