Compare commits

...

No commits in common. "4b5470a21a5200e0bd70e52e8cf4752e32cf6d0d" and "cc895ca0d50f2680205f969d2e0cdc5553d00aa2" have entirely different histories.

91 changed files with 3904 additions and 349 deletions

13
.idea/deviceManager.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

1
.idea/misc.xml generated
View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,7 +1,7 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("com.google.devtools.ksp")
id("kotlin-parcelize")
}
@ -12,7 +12,7 @@ android {
defaultConfig {
applicationId = "com.example.smartalarm"
minSdk = 26
targetSdk = 36
targetSdk = 34
versionCode = 1
versionName = "1.0"
@ -30,12 +30,12 @@ android {
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "11"
jvmTarget = "17"
}
buildFeatures {
@ -63,7 +63,7 @@ dependencies {
// Room Database
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
// DataStore
implementation("androidx.datastore:datastore-preferences:1.0.0")
@ -77,6 +77,8 @@ dependencies {
// 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")

View File

@ -2,7 +2,23 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Permissions -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<application
android:name=".AlarmApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -10,18 +26,62 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SmartAlarm">
android:theme="@style/Theme.SmartAlarm"
tools:targetApi="31">
<!-- Main Activity -->
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SmartAlarm">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Alarm Ring Activity -->
<activity
android:name=".ui.alarm.AlarmRingActivity"
android:exported="false"
android:excludeFromRecents="true"
android:launchMode="singleInstance"
android:showOnLockScreen="true"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.SmartAlarm.FullScreen"/>
<!-- Alarm Service -->
<service
android:name=".service.AlarmService"
android:exported="false"
android:enabled="true"
android:foregroundServiceType="mediaPlayback"
android:stopWithTask="false"/>
<!-- Alarm Receiver -->
<receiver
android:name=".service.AlarmReceiver"
android:exported="false"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
<!-- Boot Receiver (to reschedule alarms after reboot) -->
<receiver
android:name=".service.BootReceiver"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -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)
}
}

View File

@ -1,47 +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 androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.smartalarm.ui.theme.SmartAlarmTheme
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
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SmartAlarmTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
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()
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
private fun setupNavigation() {
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.navHostFragment) as NavHostFragment
val navController = navHostFragment.navController
binding.bottomNavigation.setupWithNavController(navController)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
SmartAlarmTheme {
Greeting("Android")
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<String>()
// 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<out String>,
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()
}
}
}
}
}

View File

@ -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<Preferences> 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<Int> = context.dataStore.data
.map { it[SHAKE_SENSITIVITY] ?: 3 }
val gradualModeDuration: Flow<Int> = context.dataStore.data
.map { it[GRADUAL_MODE_DURATION] ?: 300 } // 5 minutes in seconds
val defaultRingtone: Flow<String> = context.dataStore.data
.map { it[DEFAULT_RINGTONE] ?: "" }
val vibrateDefault: Flow<Boolean> = context.dataStore.data
.map { it[VIBRATE_DEFAULT] ?: true }
val snoozeDuration: Flow<Int> = context.dataStore.data
.map { it[SNOOZE_DURATION] ?: 5 }
val themeMode: Flow<String> = 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 }
}
}

View File

@ -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<List<AlarmEntity>>
@Query("SELECT * FROM alarms WHERE id = :id")
suspend fun getAlarmById(id: Long): AlarmEntity?
@Query("SELECT * FROM alarms WHERE isEnabled = 1")
fun getEnabledAlarms(): Flow<List<AlarmEntity>>
@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)
}

View File

@ -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
}
}
}
}

View File

@ -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<Int>): String {
return gson.toJson(value)
}
@TypeConverter
fun toIntList(value: String): List<Int> {
val type = object : TypeToken<List<Int>>() {}.type
return gson.fromJson(value, type) ?: emptyList()
}
}

View File

@ -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<List<StatisticsEntity>>
@Query("SELECT * FROM statistics WHERE alarmId = :alarmId ORDER BY triggeredAt DESC")
fun getStatisticsByAlarmId(alarmId: Long): Flow<List<StatisticsEntity>>
@Query("SELECT * FROM statistics WHERE triggeredAt >= :startTime")
fun getStatisticsSince(startTime: Long): Flow<List<StatisticsEntity>>
@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?
}

View File

@ -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<Int> = 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()
)

View File

@ -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"
)

View File

@ -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<Int> = 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] }
}
}

View File

@ -0,0 +1,8 @@
package com.example.smartalarm.data.model
enum class AlarmMode {
VIBRATE_ONLY,
SOUND_ONLY,
VIBRATE_THEN_SOUND,
GRADUAL // progressive volume
}

View File

@ -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<Int>,
val shakeToStop: Boolean = false,
val gradualMode: Boolean = false
) {
companion object {
fun getDefaultPresets(): List<AlarmPreset> {
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)
)
}
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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<List<Alarm>> = alarmDao.getAllAlarms()
.map { entities -> entities.map { AlarmMapper.toDomain(it) } }
val enabledAlarms: Flow<List<Alarm>> = 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)
}
}

View File

@ -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<List<Statistics>> = statisticsDao.getAllStatistics()
.map { entities -> entities.map { AlarmMapper.statisticsToDomain(it) } }
fun getStatisticsByAlarmId(alarmId: Long): Flow<List<Statistics>> {
return statisticsDao.getStatisticsByAlarmId(alarmId)
.map { entities -> entities.map { AlarmMapper.statisticsToDomain(it) } }
}
fun getStatisticsSince(startTime: Long): Flow<List<Statistics>> {
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
}
}

View File

@ -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
)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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<List<Alarm>> {
return repository.allAlarms
}
fun getEnabled(): Flow<List<Alarm>> {
return repository.enabledAlarms
}
}

View File

@ -0,0 +1,9 @@
package com.example.smartalarm.domain.usecase
import com.example.smartalarm.data.model.AlarmPreset
class GetPresetsUseCase {
operator fun invoke(): List<AlarmPreset> {
return AlarmPreset.getDefaultPresets()
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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
}
}

View File

@ -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<Alarm, AlarmAdapter.AlarmViewHolder>(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<Alarm>() {
override fun areItemsTheSame(oldItem: Alarm, newItem: Alarm): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Alarm, newItem: Alarm): Boolean {
return oldItem == newItem
}
}
}

View File

@ -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
}
}

View File

@ -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<Int>()
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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,224 @@
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++
// Save statistics with snooze count
val stats = Statistics(
id = statisticsId,
alarmId = alarmId,
triggeredAt = triggeredTime,
stoppedAt = null, // Not stopped yet, just snoozed
snoozedCount = snoozeCount,
stopMethod = "snooze"
)
lifecycleScope.launch {
try {
if (statisticsId > 0) {
statisticsViewModel.updateStatistics(stats)
} else {
statisticsId = statisticsViewModel.saveStatistics(stats) as? Long ?: 0L
}
} catch (e: Exception) {
e.printStackTrace()
}
}
// Stop current alarm service
stopService(android.content.Intent(this, AlarmService::class.java))
// Schedule snooze alarm for 5 minutes later
val handler = android.os.Handler(android.os.Looper.getMainLooper())
handler.postDelayed({
val snoozeIntent = android.content.Intent(applicationContext, AlarmService::class.java).apply {
putExtra("ALARM_ID", alarmId)
putExtra("ALARM_LABEL", intent.getStringExtra("ALARM_LABEL") ?: "Alarm")
putExtra("SHAKE_TO_STOP", shakeToStop)
putExtra("SHAKE_INTENSITY", shakeIntensity)
putExtra("GRADUAL_MODE", intent.getBooleanExtra("GRADUAL_MODE", false))
putExtra("VIBRATE", intent.getBooleanExtra("VIBRATE", true))
putExtra("RINGTONE_URI", intent.getStringExtra("RINGTONE_URI"))
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(snoozeIntent)
} else {
startService(snoozeIntent)
}
}, 5 * 60 * 1000) // 5 minutes
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
}
})
}
}

View File

@ -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<AlarmPreset, PresetAdapter.PresetViewHolder>(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<AlarmPreset>() {
override fun areItemsTheSame(oldItem: AlarmPreset, newItem: AlarmPreset): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: AlarmPreset, newItem: AlarmPreset): Boolean {
return oldItem == newItem
}
}
}

View File

@ -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<com.example.smartalarm.data.model.Statistics>) {
// TODO: Implement chart visualization
// This would use MPAndroidChart library
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -1,11 +0,0 @@
package com.example.smartalarm.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@ -1,58 +0,0 @@
package com.example.smartalarm.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun SmartAlarmTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@ -1,34 +0,0 @@
package com.example.smartalarm.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@ -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"
}
}
}

View File

@ -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())
}
}

View File

@ -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"
}
}
}

View File

@ -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 -> ""
}
}
}

View File

@ -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<List<Alarm>> = getAllAlarmsUseCase().asLiveData()
val enabledAlarms: LiveData<List<Alarm>> = getAllAlarmsUseCase.getEnabled().asLiveData()
private val _selectedAlarm = MutableLiveData<Alarm?>()
val selectedAlarm: LiveData<Alarm?> = _selectedAlarm
private val _operationStatus = MutableLiveData<OperationStatus>()
val operationStatus: LiveData<OperationStatus> = _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()
}
}

View File

@ -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<List<AlarmPreset>>()
val presets: LiveData<List<AlarmPreset>> = _presets
private val _selectedPreset = MutableLiveData<AlarmPreset?>()
val selectedPreset: LiveData<AlarmPreset?> = _selectedPreset
init {
loadPresets()
}
private fun loadPresets() {
_presets.value = getPresetsUseCase()
}
fun selectPreset(preset: AlarmPreset) {
_selectedPreset.value = preset
}
fun clearSelection() {
_selectedPreset.value = null
}
}

View File

@ -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<List<Statistics>> = repository.allStatistics.asLiveData()
private val _alarmStatistics = MutableLiveData<List<Statistics>>()
val alarmStatistics: LiveData<List<Statistics>> = _alarmStatistics
private val _summaryData = MutableLiveData<StatisticsSummary>()
val summaryData: LiveData<StatisticsSummary> = _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<Statistics>) {
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
)
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#E0E0E0"/>
<corners android:radius="8dp"/>
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#B2DFDB"/>
<corners android:radius="8dp"/>
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFE0B2"/>
<corners android:radius="8dp"/>
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#424242"/>
<corners android:radius="12dp"/>
<stroke
android:width="2dp"
android:color="?attr/colorPrimary"/>
</shape>

View File

@ -1,170 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorPrimary"
android:padding="24dp">
<TextView
android:id="@+id/tvAlarmTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="08:00"
android:textSize="72sp"
android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/tvAlarmLabel"
app:layout_constraintVertical_chainStyle="packed"/>
<TextView
android:id="@+id/tvAlarmLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Wake Up"
android:textSize="24sp"
android:textColor="@android:color/white"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/tvAlarmTime"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/tvShakeInfo"/>
<TextView
android:id="@+id/tvShakeInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Shake your phone to stop"
android:textSize="16sp"
android:textColor="@android:color/white"
android:layout_marginTop="32dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/tvAlarmLabel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ProgressBar
android:id="@+id/shakeProgress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone"
style="?android:attr/progressBarStyleHorizontal"
app:layout_constraintTop_toBottomOf="@id/tvShakeInfo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDismiss"
android:layout_width="0dp"
android:layout_height="64dp"
android:text="DISMISS"
android:textSize="18sp"
android:textColor="@android:color/white"
app:cornerRadius="32dp"
app:strokeColor="@android:color/white"
app:strokeWidth="2dp"
app:layout_constraintBottom_toTopOf="@id/btnSnooze"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="16dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSnooze"
android:layout_width="0dp"
android:layout_height="64dp"
android:text="SNOOZE"
android:textSize="18sp"
android:textColor="@android:color/white"
app:strokeColor="@android:color/white"
app:strokeWidth="2dp"
app:cornerRadius="32dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/navHostFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottomNavigation"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:menu="@menu/bottom_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,178 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardMain"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="16dp"
app:cardElevation="8dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
<TextView
android:id="@+id/tvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="08:00"
android:textSize="64sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/tvLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Wake Up"
android:textSize="24sp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/tvTime"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/tvRepeatDays"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Mon, Tue, Wed, Thu, Fri"
android:textSize="16sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/tvLabel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/tvTimeUntil"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rings in 8 hr 30 min"
android:textSize="14sp"
android:textColor="?attr/colorPrimary"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/tvRepeatDays"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/tvSettingsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Settings"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="@id/cardMain"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchEnabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Alarm Enabled"
android:textSize="16sp"
android:padding="16dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/tvSettingsTitle"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchVibrate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Vibrate"
android:textSize="16sp"
android:padding="16dp"
android:enabled="false"
app:layout_constraintTop_toBottomOf="@id/switchEnabled"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchShakeToStop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Shake to Stop"
android:textSize="16sp"
android:padding="16dp"
android:enabled="false"
app:layout_constraintTop_toBottomOf="@id/switchVibrate"/>
<TextView
android:id="@+id/tvShakeIntensity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Shake Intensity: 3"
android:textSize="14sp"
android:paddingStart="32dp"
android:paddingEnd="16dp"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/switchShakeToStop"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchGradualMode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Gradual Volume"
android:textSize="16sp"
android:padding="16dp"
android:enabled="false"
app:layout_constraintTop_toBottomOf="@id/tvShakeIntensity"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchSnooze"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Snooze Enabled"
android:textSize="16sp"
android:padding="16dp"
android:enabled="false"
app:layout_constraintTop_toBottomOf="@id/switchGradualMode"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="32dp"
app:layout_constraintTop_toBottomOf="@id/switchSnooze">
<Button
android:id="@+id/btnEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Edit"
android:layout_marginEnd="8dp"/>
<Button
android:id="@+id/btnDelete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Delete"
android:layout_marginStart="8dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
app:cardElevation="4dp"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center">
<TextView
android:id="@+id/tvSelectedTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="08:00"
android:textSize="56sp"
android:textStyle="bold"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPickTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Change Time"
android:layout_marginTop="16dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layoutLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Label"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/cardTime">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvRepeatLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Repeat"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="@id/layoutLabel"
app:layout_constraintStart_toStartOf="parent"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroupDays"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:singleSelection="false"
app:layout_constraintTop_toBottomOf="@id/tvRepeatLabel"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchVibrate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Vibrate"
android:textSize="16sp"
android:padding="16dp"
android:checked="true"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/chipGroupDays"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchShakeToStop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Shake to Stop"
android:textSize="16sp"
android:padding="16dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/switchVibrate"/>
<LinearLayout
android:id="@+id/layoutShakeSettings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@drawable/bg_shake_settings"
android:visibility="gone"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintTop_toBottomOf="@id/switchShakeToStop">
<TextView
android:id="@+id/tvShakeIntensity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Intensity: 3"
android:textSize="14sp"
android:textColor="@android:color/white"
android:textStyle="bold"/>
<SeekBar
android:id="@+id/seekBarShakeIntensity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:progressTint="?attr/colorPrimary"
android:thumbTint="?attr/colorPrimary"/>
</LinearLayout>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchGradualMode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Gradual Volume Increase"
android:textSize="16sp"
android:padding="16dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/layoutShakeSettings"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchSnooze"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Enable Snooze"
android:textSize="16sp"
android:padding="16dp"
android:checked="true"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/switchGradualMode"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="@id/switchSnooze"
app:layout_constraintBottom_toBottomOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Cancel"
android:layout_marginEnd="8dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSave"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Save"
android:layout_marginStart="8dp"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp"/>
<TextView
android:id="@+id/emptyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No alarms yet\nTap + to add one"
android:textSize="18sp"
android:textAlignment="center"
android:gravity="center"
android:layout_gravity="center"
android:visibility="gone"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAddAlarm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="Add Alarm"
app:srcCompat="@android:drawable/ic_input_add"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Alarm Presets"
android:textSize="24sp"
android:textStyle="bold"
android:padding="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:padding="8dp"
android:clipToPadding="false"
app:layout_constraintTop_toBottomOf="@id/tvTitle"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,197 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Alarm Statistics"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroupFilter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:singleSelection="true"
app:layout_constraintTop_toBottomOf="@id/tvTitle">
<com.google.android.material.chip.Chip
android:id="@+id/chipWeek"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Last 7 Days"
android:checked="true"
style="@style/Widget.MaterialComponents.Chip.Choice"/>
<com.google.android.material.chip.Chip
android:id="@+id/chipMonth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Last 30 Days"
style="@style/Widget.MaterialComponents.Chip.Choice"/>
<com.google.android.material.chip.Chip
android:id="@+id/chipAll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="All Time"
style="@style/Widget.MaterialComponents.Chip.Choice"/>
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardStats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp"
app:layout_constraintTop_toBottomOf="@id/chipGroupFilter">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="@+id/tvTotalAlarms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="32sp"
android:textStyle="bold"
android:textColor="?attr/colorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total Alarms"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="@+id/tvTotalSnoozes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="32sp"
android:textStyle="bold"
android:textColor="?attr/colorSecondary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total Snoozes"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
android:layout_marginVertical="16dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="@+id/tvShakeStops"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="32sp"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Shake Stops"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="@+id/tvAvgDuration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0 min"
android:textSize="32sp"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Avg Duration"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardElevation="4dp"
app:cardCornerRadius="12dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/tvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="08:00"
android:textSize="42sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchEnabled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/tvTime"
app:layout_constraintBottom_toBottomOf="@id/tvTime"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/tvLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Wake Up"
android:textSize="14sp"
android:layout_marginTop="4dp"
android:singleLine="true"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/tvTime"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btnDelete"/>
<TextView
android:id="@+id/tvRepeatDays"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Mon, Tue, Wed, Thu, Fri"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginTop="2dp"
android:singleLine="true"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/tvLabel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btnDelete"/>
<LinearLayout
android:id="@+id/layoutBadges"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="6dp"
app:layout_constraintTop_toBottomOf="@id/tvRepeatDays"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btnDelete">
<TextView
android:id="@+id/tvTimeUntil"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="in 8 hr 30 min"
android:textSize="11sp"
android:textColor="?attr/colorPrimary"
android:textStyle="bold"/>
<TextView
android:id="@+id/badgeShake"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🤚 Shake"
android:textSize="10sp"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:background="@drawable/bg_badge_shake"
android:layout_marginStart="8dp"
android:visibility="gone"/>
<TextView
android:id="@+id/badgeGradual"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📈 Gradual"
android:textSize="10sp"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:background="@drawable/bg_badge_gradual"
android:layout_marginStart="4dp"
android:visibility="gone"/>
</LinearLayout>
<ImageButton
android:id="@+id/btnDelete"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@android:drawable/ic_menu_delete"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Delete"
app:layout_constraintTop_toBottomOf="@id/switchEnabled"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/tvPresetName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Morning"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/tvPresetTime"/>
<TextView
android:id="@+id/tvPresetTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="06:30"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="?attr/colorPrimary"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/tvPresetLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Wake Up"
android:textSize="14sp"
android:layout_marginTop="4dp"
app:layout_constraintTop_toBottomOf="@id/tvPresetName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/tvPresetDays"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Mon, Tue, Wed, Thu, Fri"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginTop="4dp"
app:layout_constraintTop_toBottomOf="@id/tvPresetLabel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/alarmListFragment"
android:icon="@android:drawable/ic_lock_idle_alarm"
android:title="Alarms"/>
<item
android:id="@+id/alarmPresetFragment"
android:icon="@android:drawable/ic_menu_add"
android:title="Presets"/>
<item
android:id="@+id/statisticsFragment"
android:icon="@android:drawable/ic_menu_info_details"
android:title="Statistics"/>
</menu>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/alarmListFragment">
<fragment
android:id="@+id/alarmListFragment"
android:name="com.example.smartalarm.ui.alarm.AlarmListFragment"
android:label="Alarms">
<action
android:id="@+id/action_alarmList_to_alarmDetail"
app:destination="@id/alarmDetailFragment"/>
<action
android:id="@+id/action_alarmList_to_alarmEdit"
app:destination="@id/alarmEditFragment"/>
</fragment>
<fragment
android:id="@+id/alarmDetailFragment"
android:name="com.example.smartalarm.ui.alarm.AlarmDetailFragment"
android:label="Alarm Details">
<action
android:id="@+id/action_alarmDetail_to_alarmEdit"
app:destination="@id/alarmEditFragment"/>
</fragment>
<fragment
android:id="@+id/alarmEditFragment"
android:name="com.example.smartalarm.ui.alarm.AlarmEditFragment"
android:label="Edit Alarm"/>
<fragment
android:id="@+id/alarmPresetFragment"
android:name="com.example.smartalarm.ui.alarm.AlarmPresetFragment"
android:label="Presets"/>
<fragment
android:id="@+id/statisticsFragment"
android:name="com.example.smartalarm.ui.statistics.StatisticsFragment"
android:label="Statistics"/>
</navigation>

View File

@ -7,4 +7,14 @@
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="blue_500">#FF2196F3</color>
<color name="blue_700">#FF1976D2</color>
<!-- Badge colors -->
<color name="badge_shake">#FFE0B2</color>
<color name="badge_gradual">#B2DFDB</color>
<!-- Background colors -->
<color name="bg_shake_settings">#424242</color>
<color name="bg_shake_border">#2196F3</color>
</resources>

View File

@ -1,3 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Smart Alarm</string>
<string name="alarm_ringing">Alarm Ringing</string>
<string name="dismiss">Dismiss</string>
<string name="snooze">Snooze</string>
<string name="shake_to_stop">Shake to Stop</string>
<string name="add_alarm">Add Alarm</string>
<string name="edit_alarm">Edit Alarm</string>
<string name="delete_alarm">Delete Alarm</string>
<string name="alarm_label">Alarm Label</string>
<string name="repeat">Repeat</string>
<string name="once">Once</string>
<string name="every_day">Every day</string>
<string name="vibrate">Vibrate</string>
<string name="gradual_volume">Gradual Volume</string>
<string name="shake_intensity">Shake Intensity</string>
<string name="statistics">Statistics</string>
<string name="presets">Presets</string>
<string name="alarms">Alarms</string>
</resources>

View File

@ -1,5 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.SmartAlarm" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/blue_500</item>
<item name="colorPrimaryVariant">@color/blue_700</item>
<item name="colorOnPrimary">@color/white</item>
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
</style>
<style name="Theme.SmartAlarm" parent="android:Theme.Material.Light.NoActionBar" />
<style name="Theme.SmartAlarm.FullScreen" parent="Theme.SmartAlarm">
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowShowWallpaper">false</item>
</style>
</resources>

View File

@ -7,8 +7,8 @@ buildscript {
}
plugins {
id("com.android.application") version "8.2.0" apply false
id("com.android.library") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
id("org.jetbrains.kotlin.kapt") version "1.9.20" apply false
id("com.android.application") version "8.7.3" apply false
id("com.android.library") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("com.google.devtools.ksp") version "2.1.0-1.0.29" apply false
}

View File

@ -1,24 +1,18 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
# Performance optimizations
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
# Kotlin compiler
kotlin.daemon.jvmargs=-Xmx2048m
# Fix KAPT issues
kapt.use.worker.api=true
kapt.incremental.apt=true

View File

@ -1,6 +1,7 @@
#Tue Dec 09 07:50:57 WIB 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists