diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 103e00cb..44ca2d9b 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -3,30 +3,39 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3669286f..11b9e671 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,53 +1,60 @@
+ xmlns:tools="http://schemas.android.com/tools">
+
+
+ android:name=".App"
+ android:allowBackup="true"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.ClockYou"
+ tools:targetApi="33">
+ android:name=".ui.MainActivity"
+ android:exported="true"
+ android:theme="@style/Theme.ClockYou">
+
-
-
+
-
+ android:name=".receivers.AlarmReceiver"
+ android:exported="false" />
+ android:name=".receivers.BootReceiver"
+ android:directBootAware="true"
+ android:enabled="true"
+ android:exported="true">
@@ -57,50 +64,50 @@
-
+ android:name=".services.StopwatchService"
+ android:exported="false" />
+
+ android:name=".services.AlarmService"
+ android:exported="false" />
+ android:name=".widgets.DigitalClockWidget"
+ android:enabled="true"
+ android:exported="false">
+ android:name="android.appwidget.provider"
+ android:resource="@xml/digital_clock_widget" />
-
+ android:name=".widgets.VerticalClockWidget"
+ android:enabled="true"
+ android:exported="false">
+ android:name="android.appwidget.provider"
+ android:resource="@xml/vertical_clock_widget" />
-
+ android:name=".widgets.AnalogClockWidget"
+ android:enabled="true"
+ android:exported="false">
+ android:name="android.appwidget.provider"
+ android:resource="@xml/analog_clock_widget" />
diff --git a/app/src/main/java/com/bnyro/clock/receivers/AlarmReceiver.kt b/app/src/main/java/com/bnyro/clock/receivers/AlarmReceiver.kt
index 3b78e290..3cdb23db 100644
--- a/app/src/main/java/com/bnyro/clock/receivers/AlarmReceiver.kt
+++ b/app/src/main/java/com/bnyro/clock/receivers/AlarmReceiver.kt
@@ -1,22 +1,14 @@
package com.bnyro.clock.receivers
-import android.Manifest
-import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import android.content.pm.PackageManager
import android.util.Log
-import android.widget.Toast
-import androidx.core.app.ActivityCompat
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import androidx.core.net.toUri
-import com.bnyro.clock.R
+import androidx.core.content.ContextCompat
import com.bnyro.clock.db.DatabaseHolder
-import com.bnyro.clock.obj.Alarm
-import com.bnyro.clock.ui.MainActivity
-import com.bnyro.clock.util.*
+import com.bnyro.clock.services.AlarmService
+import com.bnyro.clock.util.AlarmHelper
+import com.bnyro.clock.util.TimeHelper
import kotlinx.coroutines.runBlocking
class AlarmReceiver : BroadcastReceiver() {
@@ -30,45 +22,12 @@ class AlarmReceiver : BroadcastReceiver() {
val currentDay = TimeHelper.getCurrentWeekDay()
if (currentDay - 1 in alarm.days) {
- Toast.makeText(context, context.getString(R.string.alarm), Toast.LENGTH_LONG).show()
- showNotification(context, alarm)
+ val playAlarm = Intent(context, AlarmService::class.java)
+ playAlarm.putExtra(AlarmHelper.EXTRA_ID, id)
+ ContextCompat.startForegroundService(context, playAlarm)
}
// re-enqueue the alarm for the next day
AlarmHelper.enqueue(context, alarm)
}
-
- private fun showNotification(context: Context, alarm: Alarm) {
- val pendingIntent = PendingIntent.getActivity(
- context,
- 0,
- Intent(context, MainActivity::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- },
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
-
- NotificationHelper.createAlarmNotificationChannel(context, alarm)
-
- val builder = NotificationCompat.Builder(context, NotificationHelper.ALARM_CHANNEL)
- .setSmallIcon(R.drawable.ic_notification)
- .setContentTitle(alarm.label ?: context.getString(R.string.alarm))
- .setAutoCancel(true)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setVibrate(if (alarm.vibrate) NotificationHelper.vibrationPattern else null)
- .setFullScreenIntent(pendingIntent, true)
- .setSound(alarm.soundUri?.toUri() ?: RingtoneHelper.getDefault(context))
- val notification = builder.build().apply {
- flags = NotificationCompat.FLAG_INSISTENT
- }
-
- if (ActivityCompat.checkSelfPermission(
- context,
- Manifest.permission.POST_NOTIFICATIONS
- ) == PackageManager.PERMISSION_GRANTED
- ) {
- NotificationManagerCompat.from(context).notify(alarm.id.toInt(), notification)
- }
- }
}
diff --git a/app/src/main/java/com/bnyro/clock/services/AlarmService.kt b/app/src/main/java/com/bnyro/clock/services/AlarmService.kt
new file mode 100644
index 00000000..2ae5bff4
--- /dev/null
+++ b/app/src/main/java/com/bnyro/clock/services/AlarmService.kt
@@ -0,0 +1,141 @@
+package com.bnyro.clock.services
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.media.MediaPlayer
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.os.Vibrator
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.net.toUri
+import com.bnyro.clock.R
+import com.bnyro.clock.db.DatabaseHolder
+import com.bnyro.clock.obj.Alarm
+import com.bnyro.clock.ui.AlarmActivity
+import com.bnyro.clock.util.AlarmHelper
+import com.bnyro.clock.util.NotificationHelper
+import kotlinx.coroutines.runBlocking
+
+class AlarmService : Service() {
+ private var isPlaying = false
+ private var vibrator: Vibrator? = null
+ private var mediaPlayer: MediaPlayer? = null
+ private var currentAlarm: Alarm? = null
+
+ private val handler: Handler = Handler(Looper.getMainLooper())
+
+ override fun onCreate() {
+ vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+ super.onCreate()
+ }
+
+ override fun onDestroy() {
+ stop()
+ Log.d("Alarm Service", "Destroying service")
+ super.onDestroy()
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ val id = intent.getLongExtra(AlarmHelper.EXTRA_ID, -1).takeIf { it != -1L }
+ ?: return START_STICKY
+ val alarm = runBlocking {
+ DatabaseHolder.instance.alarmsDao().findById(id)
+ }
+ startForeground(1, createNotification(this, alarm))
+ play(alarm)
+ currentAlarm = alarm
+ return START_STICKY
+ }
+
+ private fun play(alarm: Alarm) {
+ // stop() checks to see if we are already playing.
+ stop()
+ val alert: Uri? = alarm.soundUri?.toUri() ?: RingtoneManager.getDefaultUri(
+ RingtoneManager.TYPE_ALARM
+ )
+ mediaPlayer = MediaPlayer()
+ mediaPlayer!!.setOnErrorListener { mp, _, _ ->
+ Log.e("Media Player", "Error occurred while playing audio.")
+ mp.stop()
+ mp.release()
+ mediaPlayer = null
+ true
+ }
+ try {
+ mediaPlayer!!.setDataSource(this, alert!!)
+ startAlarm(mediaPlayer!!)
+ } catch (e: Exception) {
+ Log.e("Failed to play ringtone", e.message, e)
+ }
+
+ /* Start the vibrator after everything is ok with the media player */
+ if (alarm.vibrate) {
+ vibrator!!.vibrate(NotificationHelper.vibrationPattern, 0)
+ } else {
+ vibrator!!.cancel()
+ }
+ isPlaying = true
+ }
+
+ private fun startAlarm(player: MediaPlayer) {
+ player.isLooping = true
+ player.setAudioAttributes(NotificationHelper.audioAttributes)
+ player.prepare()
+ player.start()
+ }
+
+ /**
+ * Stops alarm
+ */
+ fun stop() {
+ if (!isPlaying) return
+ isPlaying = false
+
+ // Stop audio playing
+ if (mediaPlayer != null) {
+ mediaPlayer?.stop()
+ mediaPlayer?.release()
+ mediaPlayer = null
+ }
+
+ // Stop vibrator
+ vibrator?.cancel()
+ }
+
+ private fun createNotification(context: Context, alarm: Alarm): Notification {
+ NotificationHelper.createAlarmNotificationChannel(context, alarm)
+
+ val pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(context, AlarmActivity::class.java).apply {
+ addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK
+ or Intent.FLAG_ACTIVITY_NO_USER_ACTION
+ )
+ putExtra(AlarmHelper.EXTRA_ID, alarm.id)
+ },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val builder = NotificationCompat.Builder(context, NotificationHelper.ALARM_CHANNEL)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setContentTitle(alarm.label ?: context.getString(R.string.alarm))
+ .setAutoCancel(true)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_ALARM)
+ .setFullScreenIntent(pendingIntent, true)
+ return builder.build()
+ }
+}
diff --git a/app/src/main/java/com/bnyro/clock/ui/AlarmActivity.kt b/app/src/main/java/com/bnyro/clock/ui/AlarmActivity.kt
new file mode 100644
index 00000000..65eeb45d
--- /dev/null
+++ b/app/src/main/java/com/bnyro/clock/ui/AlarmActivity.kt
@@ -0,0 +1,69 @@
+package com.bnyro.clock.ui
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.Window
+import android.view.WindowManager
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.bnyro.clock.db.DatabaseHolder
+import com.bnyro.clock.obj.Alarm
+import com.bnyro.clock.services.AlarmService
+import com.bnyro.clock.ui.screens.AlarmAlertScreen
+import com.bnyro.clock.util.AlarmHelper
+import kotlinx.coroutines.runBlocking
+
+class AlarmActivity : ComponentActivity() {
+ private var alarm by mutableStateOf(Alarm(0, 0))
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ requestWindowFeature(Window.FEATURE_NO_TITLE)
+
+ window.addFlags(windowFlags)
+
+ setContent {
+ AlarmAlertScreen(
+ onDismiss = this@AlarmActivity::dismiss,
+ onSnooze = this@AlarmActivity::snooze,
+ label = alarm.label
+ )
+ }
+
+ handleIntent(intent)
+ }
+
+ private fun dismiss() {
+ stopService(
+ Intent(
+ this@AlarmActivity.applicationContext,
+ AlarmService::class.java
+ )
+ )
+ this@AlarmActivity.finish()
+ }
+
+ private fun snooze() {
+ dismiss()
+ AlarmHelper.snooze(this@AlarmActivity, alarm)
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ handleIntent(intent)
+ super.onNewIntent(intent)
+ }
+
+ private fun handleIntent(intent: Intent) {
+ val id = intent.getLongExtra(AlarmHelper.EXTRA_ID, -1).takeIf { it != -1L } ?: return
+ this.alarm = runBlocking {
+ DatabaseHolder.instance.alarmsDao().findById(id)
+ }
+ }
+
+ companion object {
+ private const val windowFlags =
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ }
+}
diff --git a/app/src/main/java/com/bnyro/clock/ui/screens/AlarmAlertScreen.kt b/app/src/main/java/com/bnyro/clock/ui/screens/AlarmAlertScreen.kt
new file mode 100644
index 00000000..2f8b77ac
--- /dev/null
+++ b/app/src/main/java/com/bnyro/clock/ui/screens/AlarmAlertScreen.kt
@@ -0,0 +1,135 @@
+package com.bnyro.clock.ui.screens
+
+import android.content.res.Configuration
+import androidx.compose.animation.core.Ease
+import androidx.compose.animation.core.EaseInOutBack
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Snooze
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.bnyro.clock.R
+import com.bnyro.clock.ui.theme.ClockYouTheme
+
+@Composable
+fun AlarmAlertScreen(onDismiss: () -> Unit, onSnooze: () -> Unit, label: String? = null) {
+ ClockYouTheme(darkTheme = true) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.SpaceEvenly,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ val infiniteTransition = rememberInfiniteTransition(label = "")
+ val rotation by infiniteTransition.animateFloat(
+ initialValue = -10F,
+ targetValue = 10F,
+ animationSpec = infiniteRepeatable(
+ animation = tween(400, easing = EaseInOutBack),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = ""
+ )
+ val offset by infiniteTransition.animateFloat(
+ initialValue = 10F,
+ targetValue = -10F,
+ animationSpec = infiniteRepeatable(
+ animation = tween(200, easing = Ease),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = ""
+ )
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .offset(y = offset.dp)
+ .rotate(rotation)
+ ) {
+ Image(
+ modifier = Modifier.size(250.dp),
+ painter = painterResource(id = R.drawable.ic_alarm),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary)
+ )
+ }
+ label?.let {
+ Text(text = it, style = MaterialTheme.typography.headlineMedium)
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ OutlinedButton(
+ onClick = {
+ onSnooze.invoke()
+ }
+ ) {
+ Row(Modifier.padding(8.dp)) {
+ Icon(imageVector = Icons.Rounded.Snooze, contentDescription = null)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = stringResource(R.string.snooze),
+ style = MaterialTheme.typography.titleLarge
+ )
+ }
+ }
+ TextButton(
+ onClick = {
+ onDismiss.invoke()
+ }
+ ) {
+ Text(
+ text = stringResource(R.string.dismiss),
+ style = MaterialTheme.typography.titleLarge
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview(
+ showBackground = true,
+ uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL,
+ device = "spec:width=411dp,height=891dp",
+ showSystemUi = true
+)
+@Composable
+private fun DefaultPreview() {
+ AlarmAlertScreen(onDismiss = {}, onSnooze = {}, label = "Test Alarm")
+}
diff --git a/app/src/main/java/com/bnyro/clock/util/AlarmHelper.kt b/app/src/main/java/com/bnyro/clock/util/AlarmHelper.kt
index 381fd34d..30ac02cc 100644
--- a/app/src/main/java/com/bnyro/clock/util/AlarmHelper.kt
+++ b/app/src/main/java/com/bnyro/clock/util/AlarmHelper.kt
@@ -84,4 +84,20 @@ object AlarmHelper {
}
return calendar.timeInMillis
}
+
+ fun snooze(context: Context, oldAlarm: Alarm) {
+ val snoozeMinutes = Preferences.instance.getInt(
+ Preferences.snoozeTimeMinutesKey,
+ 10
+ )
+ val calendar = GregorianCalendar()
+ val nowEpoch = calendar.timeInMillis
+ calendar.set(Calendar.HOUR_OF_DAY, 0)
+ calendar.set(Calendar.MINUTE, 0)
+ calendar.set(Calendar.SECOND, 0)
+ calendar.set(Calendar.MILLISECOND, 0)
+ val todayEpoch = calendar.timeInMillis
+ val snoozeTime = nowEpoch - todayEpoch + 1000 * 60 * snoozeMinutes
+ enqueue(context, oldAlarm.copy(time = snoozeTime))
+ }
}
diff --git a/app/src/main/java/com/bnyro/clock/util/NotificationHelper.kt b/app/src/main/java/com/bnyro/clock/util/NotificationHelper.kt
index 5732e5f8..2f819a6c 100644
--- a/app/src/main/java/com/bnyro/clock/util/NotificationHelper.kt
+++ b/app/src/main/java/com/bnyro/clock/util/NotificationHelper.kt
@@ -20,7 +20,7 @@ object NotificationHelper {
val vibrationPattern = longArrayOf(1000, 1000, 1000, 1000, 1000)
- private val audioAttributes = AudioAttributes.Builder()
+ val audioAttributes: AudioAttributes? = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
diff --git a/app/src/main/java/com/bnyro/clock/util/Preferences.kt b/app/src/main/java/com/bnyro/clock/util/Preferences.kt
index 1d443840..950e97a5 100644
--- a/app/src/main/java/com/bnyro/clock/util/Preferences.kt
+++ b/app/src/main/java/com/bnyro/clock/util/Preferences.kt
@@ -13,6 +13,7 @@ object Preferences {
const val timerShowExamplesKey = "timerShowExamples"
const val clockSortOrder = "clockSortOrder"
const val persistentTimerKey = "persistentTimers"
+ const val snoozeTimeMinutesKey = "snoozeTimeMinutes"
fun init(context: Context) {
instance = context.getSharedPreferences("clock_you", Context.MODE_PRIVATE)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 05d4553a..61500081 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -59,4 +59,6 @@
Hours
Minutes
Seconds
+ Snooze
+ Dismiss
\ No newline at end of file