commit 44ac4358532fb7de6a63b41341eb63c0bbd33a25 Author: RafiFattan23 Date: Sun Dec 28 20:38:37 2025 +0700 Testing diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..fe95010 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Smart Alarm \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..dfac544 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,86 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") + id("kotlin-parcelize") +} + +android { + namespace = "com.example.smartalarm" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.smartalarm" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + viewBinding = true + dataBinding = true + } +} + +dependencies { + // Core Android + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + + // Lifecycle & ViewModel + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + + // Navigation + implementation("androidx.navigation:navigation-fragment-ktx:2.7.6") + implementation("androidx.navigation:navigation-ui-ktx:2.7.6") + + // Room Database + implementation("androidx.room:room-runtime:2.6.1") + implementation("androidx.room:room-ktx:2.6.1") + ksp("androidx.room:room-compiler:2.6.1") + + // DataStore + implementation("androidx.datastore:datastore-preferences:1.0.0") + + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + + // WorkManager (untuk alarm yang lebih reliable) + implementation("androidx.work:work-runtime-ktx:2.9.0") + + // Chart/Graph (untuk statistik) + implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") + + implementation("com.google.code.gson:gson:2.10.1") + + // Testing + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..15b3eb0 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,22 @@ +# Add project specific ProGuard rules here. +-keepattributes *Annotation* +-keepattributes SourceFile,LineNumberTable +-keep public class * extends java.lang.Exception + +# Room +-keep class * extends androidx.room.RoomDatabase +-keep @androidx.room.Entity class * +-dontwarn androidx.room.paging.** + +# Kotlin +-keep class kotlin.** { *; } +-keep class org.jetbrains.** { *; } + +# Coroutines +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} + +# DataStore +-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { + ; +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/smartalarm/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/smartalarm/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..3b8e9fd --- /dev/null +++ b/app/src/androidTest/java/com/example/smartalarm/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.smartalarm + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.smartalarm", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fe82576 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/AlarmApplication.kt b/app/src/main/java/com/example/smartalarm/AlarmApplication.kt new file mode 100644 index 0000000..097caa6 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/AlarmApplication.kt @@ -0,0 +1,15 @@ +// AlarmApplication.kt +package com.example.smartalarm + +import android.app.Application +import com.example.smartalarm.utils.NotificationHelper + +class AlarmApplication : Application() { + + override fun onCreate() { + super.onCreate() + + // Initialize notification channel + NotificationHelper.createNotificationChannel(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/MainActivity.kt b/app/src/main/java/com/example/smartalarm/MainActivity.kt new file mode 100644 index 0000000..dc59754 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/MainActivity.kt @@ -0,0 +1,159 @@ +package com.example.smartalarm + +import android.Manifest +import android.app.AlarmManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.setupWithNavController +import com.example.smartalarm.databinding.ActivityMainBinding +import com.example.smartalarm.utils.NotificationHelper + +class MainActivity : AppCompatActivity() { + + private lateinit var binding: ActivityMainBinding + + companion object { + private const val PERMISSION_REQUEST_CODE = 100 + private const val EXACT_ALARM_PERMISSION_REQUEST = 101 + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setupNavigation() + requestNecessaryPermissions() + checkExactAlarmPermission() + checkBatteryOptimization() + NotificationHelper.createNotificationChannel(this) + } + + private fun checkBatteryOptimization() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val pm = getSystemService(Context.POWER_SERVICE) as android.os.PowerManager + if (!pm.isIgnoringBatteryOptimizations(packageName)) { + AlertDialog.Builder(this) + .setTitle("Battery Optimization") + .setMessage("For reliable alarms, please disable battery optimization for this app.") + .setPositiveButton("Open Settings") { _, _ -> + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:$packageName") + } + try { + startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + } + } + .setNegativeButton("Later", null) + .show() + } + } + } + + private fun setupNavigation() { + val navHostFragment = supportFragmentManager + .findFragmentById(R.id.navHostFragment) as NavHostFragment + val navController = navHostFragment.navController + + binding.bottomNavigation.setupWithNavController(navController) + } + + private fun checkExactAlarmPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager + if (!alarmManager.canScheduleExactAlarms()) { + AlertDialog.Builder(this) + .setTitle("Permission Required") + .setMessage("This app needs permission to schedule exact alarms. Please grant the permission in settings.") + .setPositiveButton("Open Settings") { _, _ -> + val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply { + data = Uri.parse("package:$packageName") + } + startActivityForResult(intent, EXACT_ALARM_PERMISSION_REQUEST) + } + .setNegativeButton("Cancel", null) + .show() + } + } + } + + private fun requestNecessaryPermissions() { + val permissions = mutableListOf() + + // Notification permission for Android 13+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + permissions.add(Manifest.permission.POST_NOTIFICATIONS) + } + } + + // Add other permissions if needed + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.VIBRATE + ) != PackageManager.PERMISSION_GRANTED + ) { + permissions.add(Manifest.permission.VIBRATE) + } + + if (permissions.isNotEmpty()) { + ActivityCompat.requestPermissions( + this, + permissions.toTypedArray(), + PERMISSION_REQUEST_CODE + ) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + if (requestCode == PERMISSION_REQUEST_CODE) { + if (grantResults.any { it != PackageManager.PERMISSION_GRANTED }) { + AlertDialog.Builder(this) + .setTitle("Permissions Required") + .setMessage("This app needs notification and vibration permissions to function properly.") + .setPositiveButton("OK", null) + .show() + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == EXACT_ALARM_PERMISSION_REQUEST) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager + if (!alarmManager.canScheduleExactAlarms()) { + AlertDialog.Builder(this) + .setTitle("Permission Denied") + .setMessage("Without exact alarm permission, alarms may not work reliably.") + .setPositiveButton("OK", null) + .show() + } + } + } + } +} diff --git a/app/src/main/java/com/example/smartalarm/data/datastore/PreferencesManager.kt b/app/src/main/java/com/example/smartalarm/data/datastore/PreferencesManager.kt new file mode 100644 index 0000000..317ab71 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/datastore/PreferencesManager.kt @@ -0,0 +1,64 @@ +package com.example.smartalarm.data.datastore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private val Context.dataStore: DataStore by preferencesDataStore(name = "settings") + +class PreferencesManager(private val context: Context) { + + companion object { + val SHAKE_SENSITIVITY = intPreferencesKey("shake_sensitivity") + val GRADUAL_MODE_DURATION = intPreferencesKey("gradual_mode_duration") + val DEFAULT_RINGTONE = stringPreferencesKey("default_ringtone") + val VIBRATE_DEFAULT = booleanPreferencesKey("vibrate_default") + val SNOOZE_DURATION = intPreferencesKey("snooze_duration") + val THEME_MODE = stringPreferencesKey("theme_mode") + } + + val shakeSensitivity: Flow = context.dataStore.data + .map { it[SHAKE_SENSITIVITY] ?: 3 } + + val gradualModeDuration: Flow = context.dataStore.data + .map { it[GRADUAL_MODE_DURATION] ?: 300 } // 5 minutes in seconds + + val defaultRingtone: Flow = context.dataStore.data + .map { it[DEFAULT_RINGTONE] ?: "" } + + val vibrateDefault: Flow = context.dataStore.data + .map { it[VIBRATE_DEFAULT] ?: true } + + val snoozeDuration: Flow = context.dataStore.data + .map { it[SNOOZE_DURATION] ?: 5 } + + val themeMode: Flow = context.dataStore.data + .map { it[THEME_MODE] ?: "system" } + + suspend fun updateShakeSensitivity(value: Int) { + context.dataStore.edit { it[SHAKE_SENSITIVITY] = value } + } + + suspend fun updateGradualModeDuration(value: Int) { + context.dataStore.edit { it[GRADUAL_MODE_DURATION] = value } + } + + suspend fun updateDefaultRingtone(uri: String) { + context.dataStore.edit { it[DEFAULT_RINGTONE] = uri } + } + + suspend fun updateVibrateDefault(value: Boolean) { + context.dataStore.edit { it[VIBRATE_DEFAULT] = value } + } + + suspend fun updateSnoozeDuration(value: Int) { + context.dataStore.edit { it[SNOOZE_DURATION] = value } + } + + suspend fun updateThemeMode(mode: String) { + context.dataStore.edit { it[THEME_MODE] = mode } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/db/AlarmDao.kt b/app/src/main/java/com/example/smartalarm/data/db/AlarmDao.kt new file mode 100644 index 0000000..6a93734 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/db/AlarmDao.kt @@ -0,0 +1,32 @@ +package com.example.smartalarm.data.db + +import androidx.room.* +import com.example.smartalarm.data.db.entities.AlarmEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface AlarmDao { + @Query("SELECT * FROM alarms ORDER BY hour, minute") + fun getAllAlarms(): Flow> + + @Query("SELECT * FROM alarms WHERE id = :id") + suspend fun getAlarmById(id: Long): AlarmEntity? + + @Query("SELECT * FROM alarms WHERE isEnabled = 1") + fun getEnabledAlarms(): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAlarm(alarm: AlarmEntity): Long + + @Update + suspend fun updateAlarm(alarm: AlarmEntity) + + @Delete + suspend fun deleteAlarm(alarm: AlarmEntity) + + @Query("DELETE FROM alarms WHERE id = :id") + suspend fun deleteAlarmById(id: Long) + + @Query("UPDATE alarms SET isEnabled = :enabled WHERE id = :id") + suspend fun updateAlarmEnabled(id: Long, enabled: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/db/AlarmDatabase.kt b/app/src/main/java/com/example/smartalarm/data/db/AlarmDatabase.kt new file mode 100644 index 0000000..195a6d3 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/db/AlarmDatabase.kt @@ -0,0 +1,39 @@ +package com.example.smartalarm.data.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.example.smartalarm.data.db.entities.AlarmEntity +import com.example.smartalarm.data.db.entities.StatisticsEntity + +@Database( + entities = [AlarmEntity::class, StatisticsEntity::class], + version = 1, + exportSchema = false +) +@TypeConverters(Converters::class) +abstract class AlarmDatabase : RoomDatabase() { + abstract fun alarmDao(): AlarmDao + abstract fun statisticsDao(): StatisticsDao + + companion object { + @Volatile + private var INSTANCE: AlarmDatabase? = null + + fun getDatabase(context: Context): AlarmDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + AlarmDatabase::class.java, + "smart_alarm_database" + ) + .fallbackToDestructiveMigration() + .build() + INSTANCE = instance + instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/db/Converters.kt b/app/src/main/java/com/example/smartalarm/data/db/Converters.kt new file mode 100644 index 0000000..462b5d9 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/db/Converters.kt @@ -0,0 +1,20 @@ +package com.example.smartalarm.data.db + +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +class Converters { + private val gson = Gson() + + @TypeConverter + fun fromIntList(value: List): String { + return gson.toJson(value) + } + + @TypeConverter + fun toIntList(value: String): List { + val type = object : TypeToken>() {}.type + return gson.fromJson(value, type) ?: emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/db/StatisticsDao.kt b/app/src/main/java/com/example/smartalarm/data/db/StatisticsDao.kt new file mode 100644 index 0000000..43f062b --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/db/StatisticsDao.kt @@ -0,0 +1,32 @@ +package com.example.smartalarm.data.db + +import androidx.room.* +import com.example.smartalarm.data.db.entities.StatisticsEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface StatisticsDao { + @Query("SELECT * FROM statistics ORDER BY triggeredAt DESC LIMIT 100") + fun getAllStatistics(): Flow> + + @Query("SELECT * FROM statistics WHERE alarmId = :alarmId ORDER BY triggeredAt DESC") + fun getStatisticsByAlarmId(alarmId: Long): Flow> + + @Query("SELECT * FROM statistics WHERE triggeredAt >= :startTime") + fun getStatisticsSince(startTime: Long): Flow> + + @Insert + suspend fun insertStatistics(stats: StatisticsEntity): Long + + @Update + suspend fun updateStatistics(stats: StatisticsEntity) + + @Query("DELETE FROM statistics WHERE triggeredAt < :beforeTime") + suspend fun deleteOldStatistics(beforeTime: Long) + + @Query("SELECT COUNT(*) FROM statistics WHERE alarmId = :alarmId AND stopMethod = 'shake'") + suspend fun getShakeStopCount(alarmId: Long): Int + + @Query("SELECT AVG(snoozedCount) FROM statistics WHERE alarmId = :alarmId") + suspend fun getAverageSnoozedCount(alarmId: Long): Float? +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/db/entities/AlarmEntity.kt b/app/src/main/java/com/example/smartalarm/data/db/entities/AlarmEntity.kt new file mode 100644 index 0000000..12e81e5 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/db/entities/AlarmEntity.kt @@ -0,0 +1,26 @@ +package com.example.smartalarm.data.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.example.smartalarm.data.db.Converters + +@Entity(tableName = "alarms") +@TypeConverters(Converters::class) +data class AlarmEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val hour: Int, + val minute: Int, + val label: String = "", + val isEnabled: Boolean = true, + val repeatDays: List = emptyList(), // 0=Sun, 1=Mon, ..., 6=Sat + val ringtoneUri: String = "", + val vibrate: Boolean = true, + val shakeToStop: Boolean = false, + val shakeIntensity: Int = 3, // 1-5 + val gradualMode: Boolean = false, + val snoozeEnabled: Boolean = true, + val snoozeDuration: Int = 5, // minutes + val createdAt: Long = System.currentTimeMillis() +) \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/db/entities/StatisticsEntity.kt b/app/src/main/java/com/example/smartalarm/data/db/entities/StatisticsEntity.kt new file mode 100644 index 0000000..40c6573 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/db/entities/StatisticsEntity.kt @@ -0,0 +1,15 @@ +package com.example.smartalarm.data.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "statistics") +data class StatisticsEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val alarmId: Long, + val triggeredAt: Long, + val stoppedAt: Long? = null, + val snoozedCount: Int = 0, + val stopMethod: String = "unknown" // "button", "shake", "timeout" +) \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/model/Alarm.kt b/app/src/main/java/com/example/smartalarm/data/model/Alarm.kt new file mode 100644 index 0000000..4e2b245 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/model/Alarm.kt @@ -0,0 +1,34 @@ +package com.example.smartalarm.data.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Alarm( + val id: Long = 0, + val hour: Int, + val minute: Int, + val label: String = "", + val isEnabled: Boolean = true, + val repeatDays: List = emptyList(), + val ringtoneUri: String = "", + val vibrate: Boolean = true, + val shakeToStop: Boolean = false, + val shakeIntensity: Int = 3, + val gradualMode: Boolean = false, + val snoozeEnabled: Boolean = true, + val snoozeDuration: Int = 5, + val createdAt: Long = System.currentTimeMillis() +) : Parcelable { + fun getTimeString(): String { + return String.format("%02d:%02d", hour, minute) + } + + fun getRepeatDaysString(): String { + if (repeatDays.isEmpty()) return "Once" + if (repeatDays.size == 7) return "Every day" + + val days = listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") + return repeatDays.sorted().joinToString(", ") { days[it] } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/model/AlarmMode.kt b/app/src/main/java/com/example/smartalarm/data/model/AlarmMode.kt new file mode 100644 index 0000000..9439c36 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/model/AlarmMode.kt @@ -0,0 +1,8 @@ +package com.example.smartalarm.data.model + +enum class AlarmMode { + VIBRATE_ONLY, + SOUND_ONLY, + VIBRATE_THEN_SOUND, + GRADUAL // progressive volume +} diff --git a/app/src/main/java/com/example/smartalarm/data/model/AlarmPreset.kt b/app/src/main/java/com/example/smartalarm/data/model/AlarmPreset.kt new file mode 100644 index 0000000..dc6413a --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/model/AlarmPreset.kt @@ -0,0 +1,24 @@ +package com.example.smartalarm.data.model + +data class AlarmPreset( + val name: String, + val hour: Int, + val minute: Int, + val label: String, + val repeatDays: List, + val shakeToStop: Boolean = false, + val gradualMode: Boolean = false +) { + companion object { + fun getDefaultPresets(): List { + return listOf( + AlarmPreset("Morning", 6, 30, "Wake Up", listOf(1, 2, 3, 4, 5), true, true), + AlarmPreset("Work Start", 8, 0, "Work Time", listOf(1, 2, 3, 4, 5), false, false), + AlarmPreset("Lunch", 12, 0, "Lunch Break", listOf(1, 2, 3, 4, 5), false, false), + AlarmPreset("Afternoon Nap", 14, 0, "Power Nap", emptyList(), false, false), + AlarmPreset("Evening", 18, 0, "Finish Work", listOf(1, 2, 3, 4, 5), false, false), + AlarmPreset("Night", 22, 0, "Sleep Time", emptyList(), false, true) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/model/ShakePattern.kt b/app/src/main/java/com/example/smartalarm/data/model/ShakePattern.kt new file mode 100644 index 0000000..a1699d7 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/model/ShakePattern.kt @@ -0,0 +1,11 @@ +package com.example.smartalarm.data.model + +data class ShakePattern( + val shakeCount: Int = 0, + val intensity: Float = 0f, + val duration: Long = 0L +) { + fun isValid(requiredIntensity: Int): Boolean { + return shakeCount >= requiredIntensity && intensity >= requiredIntensity * 2f + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/model/Statistics.kt b/app/src/main/java/com/example/smartalarm/data/model/Statistics.kt new file mode 100644 index 0000000..ec3bf68 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/model/Statistics.kt @@ -0,0 +1,15 @@ +package com.example.smartalarm.data.model + +data class Statistics( + val id: Long = 0, + val alarmId: Long, + val triggeredAt: Long, + val stoppedAt: Long? = null, + val snoozedCount: Int = 0, + val stopMethod: String = "unknown" +) { + fun getDurationMinutes(): Int { + if (stoppedAt == null) return 0 + return ((stoppedAt - triggeredAt) / 60000).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/repository/AlarmRepository.kt b/app/src/main/java/com/example/smartalarm/data/repository/AlarmRepository.kt new file mode 100644 index 0000000..3d8ae0a --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/repository/AlarmRepository.kt @@ -0,0 +1,40 @@ +package com.example.smartalarm.data.repository + +import com.example.smartalarm.data.db.AlarmDao +import com.example.smartalarm.data.model.Alarm +import com.example.smartalarm.domain.mapper.AlarmMapper +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class AlarmRepository(private val alarmDao: AlarmDao) { + + val allAlarms: Flow> = alarmDao.getAllAlarms() + .map { entities -> entities.map { AlarmMapper.toDomain(it) } } + + val enabledAlarms: Flow> = alarmDao.getEnabledAlarms() + .map { entities -> entities.map { AlarmMapper.toDomain(it) } } + + suspend fun getAlarmById(id: Long): Alarm? { + return alarmDao.getAlarmById(id)?.let { AlarmMapper.toDomain(it) } + } + + suspend fun insertAlarm(alarm: Alarm): Long { + return alarmDao.insertAlarm(AlarmMapper.toEntity(alarm)) + } + + suspend fun updateAlarm(alarm: Alarm) { + alarmDao.updateAlarm(AlarmMapper.toEntity(alarm)) + } + + suspend fun deleteAlarm(alarm: Alarm) { + alarmDao.deleteAlarm(AlarmMapper.toEntity(alarm)) + } + + suspend fun deleteAlarmById(id: Long) { + alarmDao.deleteAlarmById(id) + } + + suspend fun updateAlarmEnabled(id: Long, enabled: Boolean) { + alarmDao.updateAlarmEnabled(id, enabled) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/data/repository/StatisticsRepository.kt b/app/src/main/java/com/example/smartalarm/data/repository/StatisticsRepository.kt new file mode 100644 index 0000000..9c6c76d --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/data/repository/StatisticsRepository.kt @@ -0,0 +1,43 @@ +package com.example.smartalarm.data.repository + +import com.example.smartalarm.data.db.StatisticsDao +import com.example.smartalarm.data.model.Statistics +import com.example.smartalarm.domain.mapper.AlarmMapper +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class StatisticsRepository(private val statisticsDao: StatisticsDao) { + + val allStatistics: Flow> = statisticsDao.getAllStatistics() + .map { entities -> entities.map { AlarmMapper.statisticsToDomain(it) } } + + fun getStatisticsByAlarmId(alarmId: Long): Flow> { + return statisticsDao.getStatisticsByAlarmId(alarmId) + .map { entities -> entities.map { AlarmMapper.statisticsToDomain(it) } } + } + + fun getStatisticsSince(startTime: Long): Flow> { + return statisticsDao.getStatisticsSince(startTime) + .map { entities -> entities.map { AlarmMapper.statisticsToDomain(it) } } + } + + suspend fun insertStatistics(stats: Statistics): Long { + return statisticsDao.insertStatistics(AlarmMapper.statisticsToEntity(stats)) + } + + suspend fun updateStatistics(stats: Statistics) { + statisticsDao.updateStatistics(AlarmMapper.statisticsToEntity(stats)) + } + + suspend fun deleteOldStatistics(beforeTime: Long) { + statisticsDao.deleteOldStatistics(beforeTime) + } + + suspend fun getShakeStopCount(alarmId: Long): Int { + return statisticsDao.getShakeStopCount(alarmId) + } + + suspend fun getAverageSnoozedCount(alarmId: Long): Float { + return statisticsDao.getAverageSnoozedCount(alarmId) ?: 0f + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/domain/mapper/AlarmMapper.kt b/app/src/main/java/com/example/smartalarm/domain/mapper/AlarmMapper.kt new file mode 100644 index 0000000..9a93fce --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/domain/mapper/AlarmMapper.kt @@ -0,0 +1,68 @@ +package com.example.smartalarm.domain.mapper + +import com.example.smartalarm.data.db.entities.AlarmEntity +import com.example.smartalarm.data.db.entities.StatisticsEntity +import com.example.smartalarm.data.model.Alarm +import com.example.smartalarm.data.model.Statistics + +object AlarmMapper { + fun toEntity(alarm: Alarm): AlarmEntity { + return AlarmEntity( + id = alarm.id, + hour = alarm.hour, + minute = alarm.minute, + label = alarm.label, + isEnabled = alarm.isEnabled, + repeatDays = alarm.repeatDays, + ringtoneUri = alarm.ringtoneUri, + vibrate = alarm.vibrate, + shakeToStop = alarm.shakeToStop, + shakeIntensity = alarm.shakeIntensity, + gradualMode = alarm.gradualMode, + snoozeEnabled = alarm.snoozeEnabled, + snoozeDuration = alarm.snoozeDuration, + createdAt = alarm.createdAt + ) + } + + fun toDomain(entity: AlarmEntity): Alarm { + return Alarm( + id = entity.id, + hour = entity.hour, + minute = entity.minute, + label = entity.label, + isEnabled = entity.isEnabled, + repeatDays = entity.repeatDays, + ringtoneUri = entity.ringtoneUri, + vibrate = entity.vibrate, + shakeToStop = entity.shakeToStop, + shakeIntensity = entity.shakeIntensity, + gradualMode = entity.gradualMode, + snoozeEnabled = entity.snoozeEnabled, + snoozeDuration = entity.snoozeDuration, + createdAt = entity.createdAt + ) + } + + fun statisticsToEntity(stats: Statistics): StatisticsEntity { + return StatisticsEntity( + id = stats.id, + alarmId = stats.alarmId, + triggeredAt = stats.triggeredAt, + stoppedAt = stats.stoppedAt, + snoozedCount = stats.snoozedCount, + stopMethod = stats.stopMethod + ) + } + + fun statisticsToDomain(entity: StatisticsEntity): Statistics { + return Statistics( + id = entity.id, + alarmId = entity.alarmId, + triggeredAt = entity.triggeredAt, + stoppedAt = entity.stoppedAt, + snoozedCount = entity.snoozedCount, + stopMethod = entity.stopMethod + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/domain/usecase/AddAlarmUseCase.kt b/app/src/main/java/com/example/smartalarm/domain/usecase/AddAlarmUseCase.kt new file mode 100644 index 0000000..432d84b --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/domain/usecase/AddAlarmUseCase.kt @@ -0,0 +1,10 @@ +package com.example.smartalarm.domain.usecase + +import com.example.smartalarm.data.model.Alarm +import com.example.smartalarm.data.repository.AlarmRepository + +class AddAlarmUseCase(private val repository: AlarmRepository) { + suspend operator fun invoke(alarm: Alarm): Long { + return repository.insertAlarm(alarm) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/domain/usecase/DeleteAlarmUseCase.kt b/app/src/main/java/com/example/smartalarm/domain/usecase/DeleteAlarmUseCase.kt new file mode 100644 index 0000000..bc69494 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/domain/usecase/DeleteAlarmUseCase.kt @@ -0,0 +1,14 @@ +package com.example.smartalarm.domain.usecase + +import com.example.smartalarm.data.model.Alarm +import com.example.smartalarm.data.repository.AlarmRepository + +class DeleteAlarmUseCase(private val repository: AlarmRepository) { + suspend operator fun invoke(alarm: Alarm) { + repository.deleteAlarm(alarm) + } + + suspend fun byId(id: Long) { + repository.deleteAlarmById(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/domain/usecase/GetAllAlarmUseCase.kt b/app/src/main/java/com/example/smartalarm/domain/usecase/GetAllAlarmUseCase.kt new file mode 100644 index 0000000..802b071 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/domain/usecase/GetAllAlarmUseCase.kt @@ -0,0 +1,15 @@ +package com.example.smartalarm.domain.usecase + +import com.example.smartalarm.data.model.Alarm +import com.example.smartalarm.data.repository.AlarmRepository +import kotlinx.coroutines.flow.Flow + +class GetAllAlarmsUseCase(private val repository: AlarmRepository) { + operator fun invoke(): Flow> { + return repository.allAlarms + } + + fun getEnabled(): Flow> { + return repository.enabledAlarms + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/domain/usecase/GetPresetsUseCase.kt b/app/src/main/java/com/example/smartalarm/domain/usecase/GetPresetsUseCase.kt new file mode 100644 index 0000000..7388bd2 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/domain/usecase/GetPresetsUseCase.kt @@ -0,0 +1,9 @@ +package com.example.smartalarm.domain.usecase + +import com.example.smartalarm.data.model.AlarmPreset + +class GetPresetsUseCase { + operator fun invoke(): List { + return AlarmPreset.getDefaultPresets() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/domain/usecase/SaveStatisticsUseCase.kt b/app/src/main/java/com/example/smartalarm/domain/usecase/SaveStatisticsUseCase.kt new file mode 100644 index 0000000..b034d3b --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/domain/usecase/SaveStatisticsUseCase.kt @@ -0,0 +1,14 @@ +package com.example.smartalarm.domain.usecase + +import com.example.smartalarm.data.model.Statistics +import com.example.smartalarm.data.repository.StatisticsRepository + +class SaveStatisticsUseCase(private val repository: StatisticsRepository) { + suspend operator fun invoke(stats: Statistics): Long { + return repository.insertStatistics(stats) + } + + suspend fun update(stats: Statistics) { + repository.updateStatistics(stats) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/domain/usecase/UpdateAlarmUseCase.kt b/app/src/main/java/com/example/smartalarm/domain/usecase/UpdateAlarmUseCase.kt new file mode 100644 index 0000000..26e1d35 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/domain/usecase/UpdateAlarmUseCase.kt @@ -0,0 +1,10 @@ +package com.example.smartalarm.domain.usecase + +import com.example.smartalarm.data.model.Alarm +import com.example.smartalarm.data.repository.AlarmRepository + +class UpdateAlarmUseCase(private val repository: AlarmRepository) { + suspend operator fun invoke(alarm: Alarm) { + repository.updateAlarm(alarm) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/service/AlarmReceiver.kt b/app/src/main/java/com/example/smartalarm/service/AlarmReceiver.kt new file mode 100644 index 0000000..64a03a3 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/service/AlarmReceiver.kt @@ -0,0 +1,18 @@ +package com.example.smartalarm.service + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class AlarmReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val alarmId = intent.getLongExtra("ALARM_ID", -1) + if (alarmId == -1L) return + + val serviceIntent = Intent(context, AlarmService::class.java).apply { + putExtras(intent) + } + + context.startForegroundService(serviceIntent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/service/AlarmScheduler.kt b/app/src/main/java/com/example/smartalarm/service/AlarmScheduler.kt new file mode 100644 index 0000000..24413d7 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/service/AlarmScheduler.kt @@ -0,0 +1,93 @@ +package com.example.smartalarm.service + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import com.example.smartalarm.data.model.Alarm +import java.util.* + +class AlarmScheduler(private val context: Context) { + + private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + fun scheduleAlarm(alarm: Alarm) { + val intent = Intent(context, AlarmReceiver::class.java).apply { + putExtra("ALARM_ID", alarm.id) + putExtra("ALARM_LABEL", alarm.label) + putExtra("SHAKE_TO_STOP", alarm.shakeToStop) + putExtra("SHAKE_INTENSITY", alarm.shakeIntensity) + putExtra("GRADUAL_MODE", alarm.gradualMode) + putExtra("VIBRATE", alarm.vibrate) + putExtra("RINGTONE_URI", alarm.ringtoneUri) + } + + val pendingIntent = PendingIntent.getBroadcast( + context, + alarm.id.toInt(), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val calendar = getAlarmCalendar(alarm) + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (alarmManager.canScheduleExactAlarms()) { + alarmManager.setAlarmClock( + AlarmManager.AlarmClockInfo(calendar.timeInMillis, pendingIntent), + pendingIntent + ) + } else { + // Fallback to inexact alarm + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + pendingIntent + ) + } + } else { + alarmManager.setAlarmClock( + AlarmManager.AlarmClockInfo(calendar.timeInMillis, pendingIntent), + pendingIntent + ) + } + } catch (e: Exception) { + e.printStackTrace() + // Final fallback + alarmManager.set( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + pendingIntent + ) + } + } + + fun cancelAlarm(alarmId: Long) { + val intent = Intent(context, AlarmReceiver::class.java) + val pendingIntent = PendingIntent.getBroadcast( + context, + alarmId.toInt(), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + alarmManager.cancel(pendingIntent) + } + + private fun getAlarmCalendar(alarm: Alarm): Calendar { + val calendar = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, alarm.hour) + set(Calendar.MINUTE, alarm.minute) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + + // If alarm time has passed today, schedule for tomorrow + if (calendar.timeInMillis <= System.currentTimeMillis()) { + calendar.add(Calendar.DAY_OF_MONTH, 1) + } + + return calendar + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/service/AlarmService.kt b/app/src/main/java/com/example/smartalarm/service/AlarmService.kt new file mode 100644 index 0000000..b3d3733 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/service/AlarmService.kt @@ -0,0 +1,334 @@ +package com.example.smartalarm.service + +import android.app.* +import android.content.Context +import android.content.Intent +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.MediaPlayer +import android.media.RingtoneManager +import android.net.Uri +import android.os.* +import androidx.core.app.NotificationCompat +import com.example.smartalarm.ui.alarm.AlarmRingActivity +import kotlinx.coroutines.* + +class AlarmService : Service() { + + private var mediaPlayer: MediaPlayer? = null + private var vibrator: Vibrator? = null + private var gradualVolumeJob: Job? = null + private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + private var audioManager: AudioManager? = null + private var shakeDetector: ShakeDetector? = null + + private var currentAlarmId: Long = -1 + private var shakeToStop: Boolean = false + private var shakeIntensity: Int = 3 + + companion object { + const val CHANNEL_ID = "alarm_channel" + const val NOTIFICATION_ID = 1001 + const val ACTION_STOP_ALARM = "STOP_ALARM" + } + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager + + vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator + } else { + @Suppress("DEPRECATION") + getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + ACTION_STOP_ALARM -> { + stopAlarm() + return START_NOT_STICKY + } + } + + currentAlarmId = intent?.getLongExtra("ALARM_ID", -1) ?: -1 + val label = intent?.getStringExtra("ALARM_LABEL") ?: "Alarm" + shakeToStop = intent?.getBooleanExtra("SHAKE_TO_STOP", false) ?: false + shakeIntensity = intent?.getIntExtra("SHAKE_INTENSITY", 3) ?: 3 + val gradualMode = intent?.getBooleanExtra("GRADUAL_MODE", false) ?: false + val vibrate = intent?.getBooleanExtra("VIBRATE", true) ?: true + val ringtoneUri = intent?.getStringExtra("RINGTONE_URI") + + startForeground(NOTIFICATION_ID, createNotification(label)) + + // Set audio to max volume for alarm + setAlarmVolume() + + // Play ringtone + playAlarm(ringtoneUri, gradualMode) + + // Vibrate if enabled + if (vibrate) { + startVibration() + } + + // Start shake detector if enabled + if (shakeToStop) { + startShakeDetector() + } + + // Start alarm ring activity + if (intent != null) { + val activityIntent = Intent(this, AlarmRingActivity::class.java).apply { + this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtras(intent) + } + startActivity(activityIntent) + } + + return START_STICKY + } + + private fun startShakeDetector() { + shakeDetector = ShakeDetector(this) { shakeCount, intensity -> + // Check if shake pattern is valid + if (shakeCount >= shakeIntensity && intensity >= shakeIntensity * 2f) { + // Broadcast to activity to update UI + val broadcastIntent = Intent("ALARM_SHAKE_DETECTED").apply { + putExtra("SHAKE_COUNT", shakeCount) + putExtra("SHAKE_INTENSITY", intensity) + } + sendBroadcast(broadcastIntent) + + // Stop alarm + stopAlarmByShake() + } else { + // Update progress + val progressIntent = Intent("ALARM_SHAKE_PROGRESS").apply { + putExtra("SHAKE_COUNT", shakeCount) + putExtra("REQUIRED_INTENSITY", shakeIntensity) + } + sendBroadcast(progressIntent) + } + } + shakeDetector?.start() + } + + private fun stopAlarmByShake() { + // Broadcast to activity that alarm stopped by shake + val intent = Intent("ALARM_STOPPED_BY_SHAKE") + sendBroadcast(intent) + + stopAlarm() + } + + private fun setAlarmVolume() { + try { + audioManager?.let { am -> + // Save current volume + val currentVolume = am.getStreamVolume(AudioManager.STREAM_ALARM) + val maxVolume = am.getStreamMaxVolume(AudioManager.STREAM_ALARM) + + // Set alarm volume to at least 70% + val targetVolume = (maxVolume * 0.7).toInt() + if (currentVolume < targetVolume) { + am.setStreamVolume( + AudioManager.STREAM_ALARM, + targetVolume, + 0 + ) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun playAlarm(ringtoneUriString: String?, gradual: Boolean) { + try { + // Stop existing player if any + mediaPlayer?.stop() + mediaPlayer?.release() + mediaPlayer = null + + val uri = if (!ringtoneUriString.isNullOrEmpty()) { + Uri.parse(ringtoneUriString) + } else { + // Try alarm first, then ringtone, then notification + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM) + ?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + ?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + } + + mediaPlayer = MediaPlayer().apply { + // Reset before setting data source + reset() + + setDataSource(applicationContext, uri) + + setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ALARM) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setLegacyStreamType(AudioManager.STREAM_ALARM) + .build() + ) + + isLooping = true + + // Prepare first + prepare() + + // Set volume after prepare + if (gradual) { + setVolume(0.1f, 0.1f) + } else { + setVolume(1.0f, 1.0f) + } + + // Start playing + start() + + // Start gradual increase if enabled + if (gradual) { + startGradualVolumeIncrease() + } + } + } catch (e: Exception) { + e.printStackTrace() + // Fallback to system ringtone + try { + val defaultUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + ?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + + mediaPlayer = MediaPlayer().apply { + reset() + setDataSource(applicationContext, defaultUri) + setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ALARM) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setLegacyStreamType(AudioManager.STREAM_ALARM) + .build() + ) + isLooping = true + prepare() + setVolume(1.0f, 1.0f) + start() + } + } catch (fallbackException: Exception) { + fallbackException.printStackTrace() + } + } + } + + private fun startGradualVolumeIncrease() { + gradualVolumeJob = serviceScope.launch { + var volume = 0.1f + while (volume < 1.0f && isActive) { + delay(5000) // increase every 5 seconds + volume += 0.15f + if (volume > 1.0f) volume = 1.0f + + try { + mediaPlayer?.setVolume(volume, volume) + } catch (e: Exception) { + e.printStackTrace() + break + } + } + } + } + + private fun startVibration() { + val pattern = longArrayOf(0, 1000, 1000) // vibrate 1s, pause 1s + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator?.vibrate( + VibrationEffect.createWaveform(pattern, 0), + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ALARM) + .build() + ) + } else { + @Suppress("DEPRECATION") + vibrator?.vibrate(pattern, 0) + } + } + + fun stopAlarm() { + gradualVolumeJob?.cancel() + shakeDetector?.stop() + shakeDetector = null + + try { + mediaPlayer?.stop() + } catch (e: Exception) { + e.printStackTrace() + } + + mediaPlayer?.release() + mediaPlayer = null + vibrator?.cancel() + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + } + + private fun createNotification(label: String): Notification { + val intent = Intent(this, AlarmRingActivity::class.java) + val pendingIntent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val stopIntent = Intent(this, AlarmService::class.java).apply { + action = ACTION_STOP_ALARM + } + val stopPendingIntent = PendingIntent.getService( + this, 1, stopIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Alarm Ringing") + .setContentText(label) + .setSmallIcon(android.R.drawable.ic_lock_idle_alarm) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_ALARM) + .setFullScreenIntent(pendingIntent, true) + .setOngoing(true) + .addAction( + android.R.drawable.ic_menu_close_clear_cancel, + "Stop", + stopPendingIntent + ) + .build() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + "Alarm Notifications", + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = "Alarm notifications" + setSound(null, null) + enableVibration(true) + setBypassDnd(true) + } + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onDestroy() { + super.onDestroy() + stopAlarm() + serviceScope.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/service/BootReceiver.kt b/app/src/main/java/com/example/smartalarm/service/BootReceiver.kt new file mode 100644 index 0000000..70bde93 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/service/BootReceiver.kt @@ -0,0 +1,34 @@ +package com.example.smartalarm.service + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.example.smartalarm.data.db.AlarmDatabase +import com.example.smartalarm.data.repository.AlarmRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class BootReceiver : BroadcastReceiver() { + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED || + intent.action == "android.intent.action.QUICKBOOT_POWERON") { + + // Reschedule all enabled alarms + scope.launch { + val database = AlarmDatabase.getDatabase(context) + val repository = AlarmRepository(database.alarmDao()) + val scheduler = AlarmScheduler(context) + + repository.enabledAlarms.first().forEach { alarm -> + scheduler.scheduleAlarm(alarm) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/service/ShakeDetector.kt b/app/src/main/java/com/example/smartalarm/service/ShakeDetector.kt new file mode 100644 index 0000000..11b8e1f --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/service/ShakeDetector.kt @@ -0,0 +1,92 @@ +// service/ShakeDetector.kt +package com.example.smartalarm.service + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import kotlin.math.sqrt + +class ShakeDetector( + context: Context, + private val onShakeDetected: (shakeCount: Int, intensity: Float) -> Unit +) : SensorEventListener { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + + private var shakeCount = 0 + private var lastShakeTime = 0L + private var lastX = 0f + private var lastY = 0f + private var lastZ = 0f + private var shakeIntensity = 0f + + companion object { + private const val SHAKE_THRESHOLD = 15f + private const val SHAKE_TIMEOUT = 500L + private const val SHAKE_RESET_TIMEOUT = 3000L + } + + fun start() { + accelerometer?.let { + sensorManager.registerListener( + this, + it, + SensorManager.SENSOR_DELAY_UI + ) + } + } + + fun stop() { + sensorManager.unregisterListener(this) + reset() + } + + private fun reset() { + shakeCount = 0 + shakeIntensity = 0f + lastShakeTime = 0L + } + + override fun onSensorChanged(event: SensorEvent?) { + if (event?.sensor?.type != Sensor.TYPE_ACCELEROMETER) return + + val x = event.values[0] + val y = event.values[1] + val z = event.values[2] + + val currentTime = System.currentTimeMillis() + + if (lastShakeTime > 0 && currentTime - lastShakeTime > SHAKE_RESET_TIMEOUT) { + reset() + } + + val deltaX = x - lastX + val deltaY = y - lastY + val deltaZ = z - lastZ + + val acceleration = sqrt( + deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ + ) + + if (acceleration > SHAKE_THRESHOLD) { + if (currentTime - lastShakeTime > SHAKE_TIMEOUT) { + shakeCount++ + shakeIntensity = maxOf(shakeIntensity, acceleration) + lastShakeTime = currentTime + + onShakeDetected(shakeCount, shakeIntensity) + } + } + + lastX = x + lastY = y + lastZ = z + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not needed + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmAdapter.kt b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmAdapter.kt new file mode 100644 index 0000000..52d8132 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmAdapter.kt @@ -0,0 +1,79 @@ +// ui/alarm/AlarmAdapter.kt +package com.example.smartalarm.ui.alarm + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.smartalarm.data.model.Alarm +import com.example.smartalarm.databinding.ItemAlarmBinding +import com.example.smartalarm.utils.TimeFormatter + +class AlarmAdapter( + private val onAlarmClick: (Alarm) -> Unit, + private val onToggleClick: (Alarm, Boolean) -> Unit, + private val onDeleteClick: (Alarm) -> Unit +) : ListAdapter(AlarmDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlarmViewHolder { + val binding = ItemAlarmBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return AlarmViewHolder(binding) + } + + override fun onBindViewHolder(holder: AlarmViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class AlarmViewHolder( + private val binding: ItemAlarmBinding + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(alarm: Alarm) { + binding.tvTime.text = alarm.getTimeString() + binding.tvLabel.text = alarm.label.ifEmpty { "Alarm" } + binding.tvRepeatDays.text = alarm.getRepeatDaysString() + binding.tvTimeUntil.text = TimeFormatter.getTimeUntilAlarm(alarm.hour, alarm.minute) + + binding.switchEnabled.isChecked = alarm.isEnabled + binding.switchEnabled.setOnCheckedChangeListener { _, isChecked -> + onToggleClick(alarm, isChecked) + } + + binding.root.setOnClickListener { + onAlarmClick(alarm) + } + + binding.btnDelete.setOnClickListener { + onDeleteClick(alarm) + } + + // Show badges + if (alarm.shakeToStop) { + binding.badgeShake.visibility = android.view.View.VISIBLE + } else { + binding.badgeShake.visibility = android.view.View.GONE + } + + if (alarm.gradualMode) { + binding.badgeGradual.visibility = android.view.View.VISIBLE + } else { + binding.badgeGradual.visibility = android.view.View.GONE + } + } + } + + class AlarmDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Alarm, newItem: Alarm): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Alarm, newItem: Alarm): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmDetailFragment.kt b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmDetailFragment.kt new file mode 100644 index 0000000..9f6f932 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmDetailFragment.kt @@ -0,0 +1,98 @@ +// ui/alarm/AlarmDetailFragment.kt +package com.example.smartalarm.ui.alarm + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import com.example.smartalarm.R +import com.example.smartalarm.databinding.FragmentAlarmDetailBinding +import com.example.smartalarm.utils.TimeFormatter +import com.example.smartalarm.viewmodels.AlarmViewModel +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +class AlarmDetailFragment : Fragment() { + + private var _binding: FragmentAlarmDetailBinding? = null + private val binding get() = _binding!! + + private lateinit var viewModel: AlarmViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAlarmDetailBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel = ViewModelProvider(requireActivity())[AlarmViewModel::class.java] + + setupUI() + observeViewModel() + } + + private fun setupUI() { + binding.btnEdit.setOnClickListener { + findNavController().navigate(R.id.action_alarmDetail_to_alarmEdit) + } + + binding.btnDelete.setOnClickListener { + showDeleteConfirmation() + } + } + + private fun observeViewModel() { + viewModel.selectedAlarm.observe(viewLifecycleOwner) { alarm -> + if (alarm != null) { + binding.tvTime.text = alarm.getTimeString() + binding.tvLabel.text = alarm.label.ifEmpty { "Alarm" } + binding.tvRepeatDays.text = alarm.getRepeatDaysString() + binding.tvTimeUntil.text = "Rings in ${TimeFormatter.getTimeUntilAlarm(alarm.hour, alarm.minute)}" + + binding.switchEnabled.isChecked = alarm.isEnabled + binding.switchVibrate.isChecked = alarm.vibrate + binding.switchShakeToStop.isChecked = alarm.shakeToStop + binding.switchGradualMode.isChecked = alarm.gradualMode + binding.switchSnooze.isChecked = alarm.snoozeEnabled + + if (alarm.shakeToStop) { + binding.tvShakeIntensity.visibility = View.VISIBLE + binding.tvShakeIntensity.text = "Shake Intensity: ${alarm.shakeIntensity}" + } else { + binding.tvShakeIntensity.visibility = View.GONE + } + + binding.switchEnabled.setOnCheckedChangeListener { _, isChecked -> + viewModel.toggleAlarm(alarm.id, isChecked) + } + } + } + } + + private fun showDeleteConfirmation() { + val alarm = viewModel.selectedAlarm.value ?: return + + MaterialAlertDialogBuilder(requireContext()) + .setTitle("Delete Alarm") + .setMessage("Are you sure you want to delete this alarm?") + .setPositiveButton("Delete") { _, _ -> + viewModel.deleteAlarm(alarm) + findNavController().navigateUp() + } + .setNegativeButton("Cancel", null) + .show() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmEditFragment.kt b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmEditFragment.kt new file mode 100644 index 0000000..20fd89c --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmEditFragment.kt @@ -0,0 +1,213 @@ +// ui/alarm/AlarmEditFragment.kt +package com.example.smartalarm.ui.alarm + +import android.app.TimePickerDialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.SeekBar +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import com.example.smartalarm.data.model.Alarm +import com.example.smartalarm.databinding.FragmentAlarmEditBinding +import com.example.smartalarm.viewmodels.AlarmViewModel +import com.google.android.material.chip.Chip +import java.util.* + +class AlarmEditFragment : Fragment() { + + private var _binding: FragmentAlarmEditBinding? = null + private val binding get() = _binding!! + + private lateinit var viewModel: AlarmViewModel + private var editingAlarm: Alarm? = null + + private var selectedHour = 8 + private var selectedMinute = 0 + private val selectedDays = mutableSetOf() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAlarmEditBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel = ViewModelProvider(requireActivity())[AlarmViewModel::class.java] + + setupUI() + observeViewModel() + } + + private fun setupUI() { + // Set current time as default + val calendar = Calendar.getInstance() + selectedHour = calendar.get(Calendar.HOUR_OF_DAY) + selectedMinute = calendar.get(Calendar.MINUTE) + + updateTimeDisplay() + + binding.btnPickTime.setOnClickListener { + showTimePicker() + } + + setupDayChips() + setupShakeIntensitySeekBar() + setupSaveButton() + } + + private fun showTimePicker() { + TimePickerDialog( + requireContext(), + { _, hourOfDay, minute -> + selectedHour = hourOfDay + selectedMinute = minute + updateTimeDisplay() + }, + selectedHour, + selectedMinute, + true + ).show() + } + + private fun updateTimeDisplay() { + binding.tvSelectedTime.text = String.format("%02d:%02d", selectedHour, selectedMinute) + } + + private fun setupDayChips() { + val days = listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") + + days.forEachIndexed { index, day -> + val chip = Chip(requireContext()).apply { + text = day + isCheckable = true + setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + selectedDays.add(index) + } else { + selectedDays.remove(index) + } + } + } + binding.chipGroupDays.addView(chip) + } + } + + private fun setupShakeIntensitySeekBar() { + binding.seekBarShakeIntensity.max = 5 + binding.seekBarShakeIntensity.progress = 3 + + binding.seekBarShakeIntensity.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + val intensity = maxOf(1, progress) + binding.tvShakeIntensity.text = "Intensity: $intensity" + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) {} + override fun onStopTrackingTouch(seekBar: SeekBar?) {} + }) + + // Show/hide shake settings + binding.switchShakeToStop.setOnCheckedChangeListener { _, isChecked -> + binding.layoutShakeSettings.visibility = if (isChecked) View.VISIBLE else View.GONE + } + } + + private fun setupSaveButton() { + binding.btnSave.setOnClickListener { + saveAlarm() + } + + binding.btnCancel.setOnClickListener { + findNavController().navigateUp() + } + } + + private fun saveAlarm() { + val label = binding.etLabel.text.toString() + val vibrate = binding.switchVibrate.isChecked + val shakeToStop = binding.switchShakeToStop.isChecked + val gradualMode = binding.switchGradualMode.isChecked + val snoozeEnabled = binding.switchSnooze.isChecked + + val shakeIntensity = if (shakeToStop) { + maxOf(1, binding.seekBarShakeIntensity.progress) + } else 3 + + val alarm = if (editingAlarm != null) { + editingAlarm!!.copy( + hour = selectedHour, + minute = selectedMinute, + label = label, + repeatDays = selectedDays.toList(), + vibrate = vibrate, + shakeToStop = shakeToStop, + shakeIntensity = shakeIntensity, + gradualMode = gradualMode, + snoozeEnabled = snoozeEnabled + ) + } else { + Alarm( + hour = selectedHour, + minute = selectedMinute, + label = label, + repeatDays = selectedDays.toList(), + vibrate = vibrate, + shakeToStop = shakeToStop, + shakeIntensity = shakeIntensity, + gradualMode = gradualMode, + snoozeEnabled = snoozeEnabled + ) + } + + if (editingAlarm != null) { + viewModel.updateAlarm(alarm) + } else { + viewModel.addAlarm(alarm) + } + + findNavController().navigateUp() + } + + private fun observeViewModel() { + viewModel.selectedAlarm.observe(viewLifecycleOwner) { alarm -> + if (alarm != null) { + editingAlarm = alarm + populateFields(alarm) + } + } + } + + private fun populateFields(alarm: Alarm) { + selectedHour = alarm.hour + selectedMinute = alarm.minute + updateTimeDisplay() + + binding.etLabel.setText(alarm.label) + binding.switchVibrate.isChecked = alarm.vibrate + binding.switchShakeToStop.isChecked = alarm.shakeToStop + binding.switchGradualMode.isChecked = alarm.gradualMode + binding.switchSnooze.isChecked = alarm.snoozeEnabled + binding.seekBarShakeIntensity.progress = alarm.shakeIntensity + + // Set repeat days + alarm.repeatDays.forEach { dayIndex -> + selectedDays.add(dayIndex) + (binding.chipGroupDays.getChildAt(dayIndex) as? Chip)?.isChecked = true + } + + binding.layoutShakeSettings.visibility = if (alarm.shakeToStop) View.VISIBLE else View.GONE + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmListFragment.kt b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmListFragment.kt new file mode 100644 index 0000000..cacf0b0 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmListFragment.kt @@ -0,0 +1,98 @@ +// ui/alarm/AlarmListFragment.kt +package com.example.smartalarm.ui.alarm + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.smartalarm.R +import com.example.smartalarm.databinding.FragmentAlarmListBinding +import com.example.smartalarm.viewmodels.AlarmViewModel +import com.google.android.material.snackbar.Snackbar + +class AlarmListFragment : Fragment() { + + private var _binding: FragmentAlarmListBinding? = null + private val binding get() = _binding!! + + private lateinit var viewModel: AlarmViewModel + private lateinit var adapter: AlarmAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAlarmListBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel = ViewModelProvider(requireActivity())[AlarmViewModel::class.java] + + setupRecyclerView() + setupFab() + observeViewModel() + } + + private fun setupRecyclerView() { + adapter = AlarmAdapter( + onAlarmClick = { alarm -> + viewModel.selectAlarm(alarm) + findNavController().navigate(R.id.action_alarmList_to_alarmDetail) + }, + onToggleClick = { alarm, isEnabled -> + viewModel.toggleAlarm(alarm.id, isEnabled) + }, + onDeleteClick = { alarm -> + viewModel.deleteAlarm(alarm) + } + ) + + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + } + + private fun setupFab() { + binding.fabAddAlarm.setOnClickListener { + viewModel.selectAlarm(null) + findNavController().navigate(R.id.action_alarmList_to_alarmEdit) + } + } + + private fun observeViewModel() { + viewModel.allAlarms.observe(viewLifecycleOwner) { alarms -> + adapter.submitList(alarms) + + if (alarms.isEmpty()) { + binding.emptyView.visibility = View.VISIBLE + binding.recyclerView.visibility = View.GONE + } else { + binding.emptyView.visibility = View.GONE + binding.recyclerView.visibility = View.VISIBLE + } + } + + viewModel.operationStatus.observe(viewLifecycleOwner) { status -> + when (status) { + is AlarmViewModel.OperationStatus.Success -> { + Snackbar.make(binding.root, status.message, Snackbar.LENGTH_SHORT).show() + } + is AlarmViewModel.OperationStatus.Error -> { + Snackbar.make(binding.root, status.message, Snackbar.LENGTH_LONG).show() + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmPresetFragment.kt b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmPresetFragment.kt new file mode 100644 index 0000000..36ce633 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmPresetFragment.kt @@ -0,0 +1,74 @@ +// ui/alarm/AlarmPresetFragment.kt +package com.example.smartalarm.ui.alarm + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.smartalarm.databinding.FragmentAlarmPresetBinding +import com.example.smartalarm.viewmodels.AlarmViewModel +import com.example.smartalarm.viewmodels.PresetViewModel +import com.example.smartalarm.data.model.Alarm + +class AlarmPresetFragment : Fragment() { + + private var _binding: FragmentAlarmPresetBinding? = null + private val binding get() = _binding!! + + private lateinit var presetViewModel: PresetViewModel + private lateinit var alarmViewModel: AlarmViewModel + private lateinit var adapter: PresetAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAlarmPresetBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + presetViewModel = ViewModelProvider(this)[PresetViewModel::class.java] + alarmViewModel = ViewModelProvider(requireActivity())[AlarmViewModel::class.java] + + setupRecyclerView() + observeViewModel() + } + + private fun setupRecyclerView() { + adapter = PresetAdapter { preset -> + // Convert preset to alarm and add it + val alarm = Alarm( + hour = preset.hour, + minute = preset.minute, + label = preset.label, + repeatDays = preset.repeatDays, + shakeToStop = preset.shakeToStop, + gradualMode = preset.gradualMode + ) + alarmViewModel.addAlarm(alarm) + findNavController().navigateUp() + } + + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + } + + private fun observeViewModel() { + presetViewModel.presets.observe(viewLifecycleOwner) { presets -> + adapter.submitList(presets) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmRingActivity.kt b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmRingActivity.kt new file mode 100644 index 0000000..2223899 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/ui/alarm/AlarmRingActivity.kt @@ -0,0 +1,192 @@ +package com.example.smartalarm.ui.alarm + +import android.os.Build +import android.os.Bundle +import android.view.WindowManager +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.example.smartalarm.databinding.ActivityAlarmRingBinding +import com.example.smartalarm.data.model.Statistics +import com.example.smartalarm.service.AlarmService +import com.example.smartalarm.service.ShakeDetector +import com.example.smartalarm.utils.ShakePatternAnalyzer +import com.example.smartalarm.utils.TimeFormatter +import com.example.smartalarm.viewmodels.StatisticsViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class AlarmRingActivity : AppCompatActivity() { + + private lateinit var binding: ActivityAlarmRingBinding + private lateinit var statisticsViewModel: StatisticsViewModel + private var shakeDetector: ShakeDetector? = null + + private var alarmId: Long = -1 + private var shakeToStop = false + private var shakeIntensity = 3 + private var statisticsId: Long = -1 + private var snoozeCount = 0 + private var triggeredTime = 0L + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Show activity even when locked + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true) + setTurnScreenOn(true) + } else { + window.addFlags( + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or + WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + ) + } + + binding = ActivityAlarmRingBinding.inflate(layoutInflater) + setContentView(binding.root) + + statisticsViewModel = ViewModelProvider(this)[StatisticsViewModel::class.java] + + setupFromIntent() + setupUI() + setupShakeDetector() + setupBackPressHandler() + saveInitialStatistics() + } + + private fun setupFromIntent() { + alarmId = intent.getLongExtra("ALARM_ID", -1) + val label = intent.getStringExtra("ALARM_LABEL") ?: "Alarm" + shakeToStop = intent.getBooleanExtra("SHAKE_TO_STOP", false) + shakeIntensity = intent.getIntExtra("SHAKE_INTENSITY", 3) + + binding.tvAlarmLabel.text = label + binding.tvAlarmTime.text = TimeFormatter.formatTimestamp(System.currentTimeMillis()) + + triggeredTime = System.currentTimeMillis() + } + + private fun setupUI() { + if (shakeToStop) { + binding.btnDismiss.text = "Shake to Stop" + binding.tvShakeInfo.text = "Shake your phone ${shakeIntensity} times" + binding.tvShakeInfo.visibility = android.view.View.VISIBLE + binding.shakeProgress.visibility = android.view.View.VISIBLE + binding.shakeProgress.max = shakeIntensity + } else { + binding.btnDismiss.text = "Dismiss" + binding.tvShakeInfo.visibility = android.view.View.GONE + binding.shakeProgress.visibility = android.view.View.GONE + + binding.btnDismiss.setOnClickListener { + stopAlarm("button") + } + } + + binding.btnSnooze.setOnClickListener { + snoozeAlarm() + } + + // Update clock every second + lifecycleScope.launch { + while (true) { + binding.tvAlarmTime.text = TimeFormatter.formatTimestamp(System.currentTimeMillis()) + delay(1000) + } + } + } + + private fun setupShakeDetector() { + if (!shakeToStop) return + + shakeDetector = ShakeDetector(this) { shakeCount, intensity -> + binding.shakeProgress.progress = shakeCount + binding.tvShakeInfo.text = "Shake ${shakeCount}/${shakeIntensity}" + + if (ShakePatternAnalyzer.isValidPattern(shakeCount, intensity, shakeIntensity)) { + stopAlarm("shake") + } + } + shakeDetector?.start() + } + + private fun saveInitialStatistics() { + val stats = Statistics( + alarmId = alarmId, + triggeredAt = triggeredTime, + stoppedAt = null, + snoozedCount = 0, + stopMethod = "unknown" + ) + + lifecycleScope.launch { + try { + statisticsId = statisticsViewModel.saveStatistics(stats) as? Long ?: 0L + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun stopAlarm(method: String) { + val stats = Statistics( + id = statisticsId, + alarmId = alarmId, + triggeredAt = triggeredTime, + stoppedAt = System.currentTimeMillis(), + snoozedCount = snoozeCount, + stopMethod = method + ) + + lifecycleScope.launch { + try { + if (statisticsId > 0) { + statisticsViewModel.updateStatistics(stats) + } else { + statisticsViewModel.saveStatistics(stats) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + stopService(android.content.Intent(this, AlarmService::class.java)) + finish() + } + + private fun snoozeAlarm() { + snoozeCount++ + + // Schedule snooze (typically 5 minutes) + val snoozeIntent = android.content.Intent(this, AlarmService::class.java).apply { + putExtras(intent) + } + + stopService(android.content.Intent(this, AlarmService::class.java)) + + // Reschedule for 5 minutes later + lifecycleScope.launch { + delay(5 * 60 * 1000) // 5 minutes + startForegroundService(snoozeIntent) + } + + finish() + } + + override fun onDestroy() { + super.onDestroy() + shakeDetector?.stop() + } + + private fun setupBackPressHandler() { + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + // Prevent back button from dismissing alarm + // User must use dismiss or snooze button + // Do nothing - block back press + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/ui/alarm/PresetAdapter.kt b/app/src/main/java/com/example/smartalarm/ui/alarm/PresetAdapter.kt new file mode 100644 index 0000000..66d2268 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/ui/alarm/PresetAdapter.kt @@ -0,0 +1,58 @@ +// ui/alarm/PresetAdapter.kt +package com.example.smartalarm.ui.alarm + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.smartalarm.data.model.AlarmPreset +import com.example.smartalarm.databinding.ItemPresetBinding + +class PresetAdapter( + private val onPresetClick: (AlarmPreset) -> Unit +) : ListAdapter(PresetDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PresetViewHolder { + val binding = ItemPresetBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return PresetViewHolder(binding) + } + + override fun onBindViewHolder(holder: PresetViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class PresetViewHolder( + private val binding: ItemPresetBinding + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(preset: AlarmPreset) { + binding.tvPresetName.text = preset.name + binding.tvPresetTime.text = String.format("%02d:%02d", preset.hour, preset.minute) + binding.tvPresetLabel.text = preset.label + + val days = preset.repeatDays.sorted().joinToString(", ") { + listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")[it] + } + binding.tvPresetDays.text = if (days.isEmpty()) "Once" else days + + binding.root.setOnClickListener { + onPresetClick(preset) + } + } + } + + class PresetDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AlarmPreset, newItem: AlarmPreset): Boolean { + return oldItem.name == newItem.name + } + + override fun areContentsTheSame(oldItem: AlarmPreset, newItem: AlarmPreset): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/java/com/example/smartalarm/ui/statistics/StatisticsFragment.kt b/app/src/main/java/com/example/smartalarm/ui/statistics/StatisticsFragment.kt new file mode 100644 index 0000000..5868b51 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/ui/statistics/StatisticsFragment.kt @@ -0,0 +1,87 @@ +// ui/statistics/StatisticsFragment.kt +package com.example.smartalarm.ui.statistics + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import com.example.smartalarm.databinding.FragmentStatisticsBinding +import com.example.smartalarm.viewmodels.StatisticsViewModel +import java.util.concurrent.TimeUnit + +class StatisticsFragment : Fragment() { + + private var _binding: FragmentStatisticsBinding? = null + private val binding get() = _binding!! + + private lateinit var viewModel: StatisticsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentStatisticsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel = ViewModelProvider(this)[StatisticsViewModel::class.java] + + setupUI() + observeViewModel() + loadStatistics() + } + + private fun setupUI() { + binding.chipWeek.setOnClickListener { + loadStatistics(7) + } + + binding.chipMonth.setOnClickListener { + loadStatistics(30) + } + + binding.chipAll.setOnClickListener { + loadStatistics() + } + } + + private fun loadStatistics(days: Int? = null) { + if (days != null) { + val startTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(days.toLong()) + viewModel.loadStatisticsSince(startTime) + } else { + // Load all statistics + viewModel.loadStatisticsSince(0) + } + } + + private fun observeViewModel() { + viewModel.summaryData.observe(viewLifecycleOwner) { summary -> + binding.tvTotalAlarms.text = summary.totalAlarms.toString() + binding.tvTotalSnoozes.text = summary.totalSnoozes.toString() + binding.tvShakeStops.text = summary.shakeStops.toString() + binding.tvAvgDuration.text = "${summary.averageDurationMinutes} min" + } + + viewModel.allStatistics.observe(viewLifecycleOwner) { statistics -> + // Could show a chart here using MPAndroidChart + updateChart(statistics) + } + } + + private fun updateChart(statistics: List) { + // TODO: Implement chart visualization + // This would use MPAndroidChart library + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/utils/AlarmModeController.kt b/app/src/main/java/com/example/smartalarm/utils/AlarmModeController.kt new file mode 100644 index 0000000..4b06371 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/utils/AlarmModeController.kt @@ -0,0 +1,35 @@ +// utils/AlarmModeController.kt +package com.example.smartalarm.utils + +import com.example.smartalarm.data.model.AlarmMode + +object AlarmModeController { + + fun getVolumeForGradualMode( + elapsedSeconds: Int, + maxDurationSeconds: Int + ): Float { + if (elapsedSeconds >= maxDurationSeconds) return 1.0f + + val progress = elapsedSeconds.toFloat() / maxDurationSeconds + return 0.1f + (progress * 0.9f) + } + + fun shouldVibrate(mode: AlarmMode, elapsedSeconds: Int): Boolean { + return when (mode) { + AlarmMode.VIBRATE_ONLY -> true + AlarmMode.SOUND_ONLY -> false + AlarmMode.VIBRATE_THEN_SOUND -> elapsedSeconds < 30 + AlarmMode.GRADUAL -> true + } + } + + fun getModeDescription(mode: AlarmMode): String { + return when (mode) { + AlarmMode.VIBRATE_ONLY -> "Vibration only" + AlarmMode.SOUND_ONLY -> "Sound only" + AlarmMode.VIBRATE_THEN_SOUND -> "Vibrate first, then sound" + AlarmMode.GRADUAL -> "Progressive volume increase" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/utils/NotificationHelper.kt b/app/src/main/java/com/example/smartalarm/utils/NotificationHelper.kt new file mode 100644 index 0000000..86dc5c5 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/utils/NotificationHelper.kt @@ -0,0 +1,51 @@ +// utils/NotificationHelper.kt +package com.example.smartalarm.utils + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat + +object NotificationHelper { + + private const val CHANNEL_ID = "smart_alarm_channel" + private const val CHANNEL_NAME = "Smart Alarm" + + fun createNotificationChannel(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = "Notifications for Smart Alarm app" + enableVibration(true) + enableLights(true) + } + + val notificationManager = context.getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + } + + fun showNotification( + context: Context, + title: String, + message: String, + notificationId: Int = 1 + ) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + val builder = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_lock_idle_alarm) + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true) + + notificationManager.notify(notificationId, builder.build()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/utils/ShakePatternAnalyzer.kt b/app/src/main/java/com/example/smartalarm/utils/ShakePatternAnalyzer.kt new file mode 100644 index 0000000..3f6e207 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/utils/ShakePatternAnalyzer.kt @@ -0,0 +1,38 @@ +package com.example.smartalarm.utils + +import com.example.smartalarm.data.model.ShakePattern + +object ShakePatternAnalyzer { + + fun isValidPattern( + shakeCount: Int, + intensity: Float, + requiredIntensity: Int + ): Boolean { + val pattern = ShakePattern( + shakeCount = shakeCount, + intensity = intensity, + duration = 0L + ) + return pattern.isValid(requiredIntensity) + } + + fun getProgressPercentage( + currentShakes: Int, + requiredIntensity: Int + ): Int { + return ((currentShakes.toFloat() / requiredIntensity) * 100).toInt() + .coerceIn(0, 100) + } + + fun getIntensityDescription(intensity: Int): String { + return when (intensity) { + 1 -> "Very Light" + 2 -> "Light" + 3 -> "Medium" + 4 -> "Strong" + 5 -> "Very Strong" + else -> "Unknown" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/utils/TimeFormatter.kt b/app/src/main/java/com/example/smartalarm/utils/TimeFormatter.kt new file mode 100644 index 0000000..2d99e9e --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/utils/TimeFormatter.kt @@ -0,0 +1,91 @@ +// utils/TimeFormatter.kt +package com.example.smartalarm.utils + +import java.text.SimpleDateFormat +import java.util.* + +object TimeFormatter { + + private val timeFormat24 = SimpleDateFormat("HH:mm", Locale.getDefault()) + private val timeFormat12 = SimpleDateFormat("hh:mm a", Locale.getDefault()) + private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()) + private val dateTimeFormat = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault()) + + fun formatTime24(hour: Int, minute: Int): String { + val calendar = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, hour) + set(Calendar.MINUTE, minute) + } + return timeFormat24.format(calendar.time) + } + + fun formatTime12(hour: Int, minute: Int): String { + val calendar = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, hour) + set(Calendar.MINUTE, minute) + } + return timeFormat12.format(calendar.time) + } + + fun formatTimestamp(timestamp: Long, use24Hour: Boolean = true): String { + val format = if (use24Hour) timeFormat24 else timeFormat12 + return format.format(Date(timestamp)) + } + + fun formatDate(timestamp: Long): String { + return dateFormat.format(Date(timestamp)) + } + + fun formatDateTime(timestamp: Long): String { + return dateTimeFormat.format(Date(timestamp)) + } + + fun getTimeUntilAlarm(alarmHour: Int, alarmMinute: Int): String { + val now = Calendar.getInstance() + val alarm = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, alarmHour) + set(Calendar.MINUTE, alarmMinute) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + + if (alarm.before(now)) { + alarm.add(Calendar.DAY_OF_MONTH, 1) + } + + val diffMillis = alarm.timeInMillis - now.timeInMillis + val hours = (diffMillis / (1000 * 60 * 60)).toInt() + val minutes = ((diffMillis / (1000 * 60)) % 60).toInt() + + return when { + hours > 0 -> "$hours hr $minutes min" + else -> "$minutes min" + } + } + + fun getDayName(dayIndex: Int): String { + return when (dayIndex) { + 0 -> "Sunday" + 1 -> "Monday" + 2 -> "Tuesday" + 3 -> "Wednesday" + 4 -> "Thursday" + 5 -> "Friday" + 6 -> "Saturday" + else -> "" + } + } + + fun getDayShortName(dayIndex: Int): String { + return when (dayIndex) { + 0 -> "Sun" + 1 -> "Mon" + 2 -> "Tue" + 3 -> "Wed" + 4 -> "Thu" + 5 -> "Fri" + 6 -> "Sat" + else -> "" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/viewmodels/AlarmViewModel.kt b/app/src/main/java/com/example/smartalarm/viewmodels/AlarmViewModel.kt new file mode 100644 index 0000000..0074903 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/viewmodels/AlarmViewModel.kt @@ -0,0 +1,112 @@ +// viewmodel/AlarmViewModel.kt +package com.example.smartalarm.viewmodels + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.example.smartalarm.data.db.AlarmDatabase +import com.example.smartalarm.data.model.Alarm +import com.example.smartalarm.data.repository.AlarmRepository +import com.example.smartalarm.domain.usecase.* +import com.example.smartalarm.service.AlarmScheduler +import kotlinx.coroutines.launch + +class AlarmViewModel(application: Application) : AndroidViewModel(application) { + + private val database = AlarmDatabase.getDatabase(application) + private val repository = AlarmRepository(database.alarmDao()) + + private val getAllAlarmsUseCase = GetAllAlarmsUseCase(repository) + private val addAlarmUseCase = AddAlarmUseCase(repository) + private val updateAlarmUseCase = UpdateAlarmUseCase(repository) + private val deleteAlarmUseCase = DeleteAlarmUseCase(repository) + + private val scheduler = AlarmScheduler(application) + + val allAlarms: LiveData> = getAllAlarmsUseCase().asLiveData() + val enabledAlarms: LiveData> = getAllAlarmsUseCase.getEnabled().asLiveData() + + private val _selectedAlarm = MutableLiveData() + val selectedAlarm: LiveData = _selectedAlarm + + private val _operationStatus = MutableLiveData() + val operationStatus: LiveData = _operationStatus + + fun addAlarm(alarm: Alarm) { + viewModelScope.launch { + try { + val id = addAlarmUseCase(alarm) + if (alarm.isEnabled) { + scheduler.scheduleAlarm(alarm.copy(id = id)) + } + _operationStatus.value = OperationStatus.Success("Alarm added") + } catch (e: Exception) { + _operationStatus.value = OperationStatus.Error(e.message ?: "Failed to add alarm") + } + } + } + + fun updateAlarm(alarm: Alarm) { + viewModelScope.launch { + try { + updateAlarmUseCase(alarm) + scheduler.cancelAlarm(alarm.id) + if (alarm.isEnabled) { + scheduler.scheduleAlarm(alarm) + } + _operationStatus.value = OperationStatus.Success("Alarm updated") + } catch (e: Exception) { + _operationStatus.value = OperationStatus.Error(e.message ?: "Failed to update alarm") + } + } + } + + fun deleteAlarm(alarm: Alarm) { + viewModelScope.launch { + try { + deleteAlarmUseCase(alarm) + scheduler.cancelAlarm(alarm.id) + _operationStatus.value = OperationStatus.Success("Alarm deleted") + } catch (e: Exception) { + _operationStatus.value = OperationStatus.Error(e.message ?: "Failed to delete alarm") + } + } + } + + fun toggleAlarm(alarmId: Long, enabled: Boolean) { + viewModelScope.launch { + try { + repository.updateAlarmEnabled(alarmId, enabled) + val alarm = repository.getAlarmById(alarmId) + if (alarm != null) { + if (enabled) { + scheduler.scheduleAlarm(alarm) + } else { + scheduler.cancelAlarm(alarmId) + } + } + } catch (e: Exception) { + _operationStatus.value = OperationStatus.Error(e.message ?: "Failed to toggle alarm") + } + } + } + + fun selectAlarm(alarm: Alarm?) { + _selectedAlarm.value = alarm + } + + fun getAlarmById(id: Long) { + viewModelScope.launch { + val alarm = repository.getAlarmById(id) + _selectedAlarm.value = alarm + } + } + + sealed class OperationStatus { + data class Success(val message: String) : OperationStatus() + data class Error(val message: String) : OperationStatus() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/viewmodels/PresetViewModel.kt b/app/src/main/java/com/example/smartalarm/viewmodels/PresetViewModel.kt new file mode 100644 index 0000000..60bb831 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/viewmodels/PresetViewModel.kt @@ -0,0 +1,35 @@ +// viewmodel/PresetViewModel.kt +package com.example.smartalarm.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.example.smartalarm.data.model.AlarmPreset +import com.example.smartalarm.domain.usecase.GetPresetsUseCase + +class PresetViewModel : ViewModel() { + + private val getPresetsUseCase = GetPresetsUseCase() + + private val _presets = MutableLiveData>() + val presets: LiveData> = _presets + + private val _selectedPreset = MutableLiveData() + val selectedPreset: LiveData = _selectedPreset + + init { + loadPresets() + } + + private fun loadPresets() { + _presets.value = getPresetsUseCase() + } + + fun selectPreset(preset: AlarmPreset) { + _selectedPreset.value = preset + } + + fun clearSelection() { + _selectedPreset.value = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartalarm/viewmodels/StatisticsViewModel.kt b/app/src/main/java/com/example/smartalarm/viewmodels/StatisticsViewModel.kt new file mode 100644 index 0000000..07442b5 --- /dev/null +++ b/app/src/main/java/com/example/smartalarm/viewmodels/StatisticsViewModel.kt @@ -0,0 +1,90 @@ +package com.example.smartalarm.viewmodels + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.example.smartalarm.data.db.AlarmDatabase +import com.example.smartalarm.data.model.Statistics +import com.example.smartalarm.data.repository.StatisticsRepository +import com.example.smartalarm.domain.usecase.SaveStatisticsUseCase +import kotlinx.coroutines.launch + +class StatisticsViewModel(application: Application) : AndroidViewModel(application) { + + private val database = AlarmDatabase.getDatabase(application) + private val repository = StatisticsRepository(database.statisticsDao()) + + private val saveStatisticsUseCase = SaveStatisticsUseCase(repository) + + val allStatistics: LiveData> = repository.allStatistics.asLiveData() + + private val _alarmStatistics = MutableLiveData>() + val alarmStatistics: LiveData> = _alarmStatistics + + private val _summaryData = MutableLiveData() + val summaryData: LiveData = _summaryData + + fun saveStatistics(stats: Statistics): Long { + var id = 0L + viewModelScope.launch { + id = saveStatisticsUseCase(stats) + } + return id + } + + fun updateStatistics(stats: Statistics) { + viewModelScope.launch { + saveStatisticsUseCase.update(stats) + } + } + + fun loadStatisticsForAlarm(alarmId: Long) { + viewModelScope.launch { + repository.getStatisticsByAlarmId(alarmId).asLiveData().observeForever { + _alarmStatistics.value = it + } + } + } + + fun loadStatisticsSince(startTime: Long) { + viewModelScope.launch { + repository.getStatisticsSince(startTime).asLiveData().observeForever { stats -> + calculateSummary(stats) + } + } + } + + private fun calculateSummary(statistics: List) { + val totalAlarms = statistics.size + val totalSnoozes = statistics.sumOf { it.snoozedCount } + val shakeStops = statistics.count { it.stopMethod == "shake" } + val avgDuration = if (statistics.isNotEmpty()) { + statistics.mapNotNull { it.stoppedAt?.let { stop -> (stop - it.triggeredAt) / 60000 } } + .average() + } else 0.0 + + _summaryData.value = StatisticsSummary( + totalAlarms = totalAlarms, + totalSnoozes = totalSnoozes, + shakeStops = shakeStops, + averageDurationMinutes = avgDuration.toInt() + ) + } + + fun cleanOldStatistics(daysToKeep: Int = 30) { + viewModelScope.launch { + val cutoffTime = System.currentTimeMillis() - (daysToKeep * 24 * 60 * 60 * 1000L) + repository.deleteOldStatistics(cutoffTime) + } + } + + data class StatisticsSummary( + val totalAlarms: Int, + val totalSnoozes: Int, + val shakeStops: Int, + val averageDurationMinutes: Int + ) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_badge.xml b/app/src/main/res/drawable/bg_badge.xml new file mode 100644 index 0000000..05514cf --- /dev/null +++ b/app/src/main/res/drawable/bg_badge.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_alarm_ring.xml b/app/src/main/res/layout/activity_alarm_ring.xml new file mode 100644 index 0000000..e6ac047 --- /dev/null +++ b/app/src/main/res/layout/activity_alarm_ring.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..86116af --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_alarm_detail.xml b/app/src/main/res/layout/fragment_alarm_detail.xml new file mode 100644 index 0000000..f5f20f1 --- /dev/null +++ b/app/src/main/res/layout/fragment_alarm_detail.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +