From 88c298fed5bec52247538d653a6a2e4036823465 Mon Sep 17 00:00:00 2001 From: Suhas Dissanayake Date: Mon, 30 Oct 2023 21:39:41 +0530 Subject: [PATCH 1/5] feat: add full screen intent for alarm --- app/src/main/AndroidManifest.xml | 97 +++++------ .../bnyro/clock/receivers/AlarmReceiver.kt | 54 +------ .../com/bnyro/clock/services/AlarmService.kt | 151 ++++++++++++++++++ .../java/com/bnyro/clock/ui/AlarmActivity.kt | 89 +++++++++++ 4 files changed, 298 insertions(+), 93 deletions(-) create mode 100644 app/src/main/java/com/bnyro/clock/services/AlarmService.kt create mode 100644 app/src/main/java/com/bnyro/clock/ui/AlarmActivity.kt 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..3237af9b 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,13 @@ 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 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 +21,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) + context.startService(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..05b15705 --- /dev/null +++ b/app/src/main/java/com/bnyro/clock/services/AlarmService.kt @@ -0,0 +1,151 @@ +package com.bnyro.clock.services + +import android.Manifest +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +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.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +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) + } + showNotification(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.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 showNotification(context: Context, alarm: Alarm) { + 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) + val notification = builder.build() + + 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/ui/AlarmActivity.kt b/app/src/main/java/com/bnyro/clock/ui/AlarmActivity.kt new file mode 100644 index 00000000..3376c8c7 --- /dev/null +++ b/app/src/main/java/com/bnyro/clock/ui/AlarmActivity.kt @@ -0,0 +1,89 @@ +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.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.bnyro.clock.db.DatabaseHolder +import com.bnyro.clock.obj.Alarm +import com.bnyro.clock.services.AlarmService +import com.bnyro.clock.ui.theme.ClockYouTheme +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) + + val win = window + win.addFlags( + 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 + ) + + setContent { + ClockYouTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Alarm", style = MaterialTheme.typography.displayLarge) + alarm.label?.let { + Text(text = it, style = MaterialTheme.typography.headlineMedium) + } + Row() { + Button(onClick = { + stopService( + Intent( + this@AlarmActivity.applicationContext, + AlarmService::class.java + ) + ) + this@AlarmActivity.finish() + }) { + Text(text = "Dismiss") + } + } + } + } + } + } + + handleIntent(intent) + } + + 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) + } + } +} From 4b52162b883b066b18d774c3e4f5f91c7fcd69c8 Mon Sep 17 00:00:00 2001 From: Suhas Dissanayake Date: Tue, 31 Oct 2023 20:58:40 +0530 Subject: [PATCH 2/5] feat: better alarm screen design --- .idea/inspectionProfiles/Project_Default.xml | 9 ++ .../java/com/bnyro/clock/ui/AlarmActivity.kt | 65 +++------ .../clock/ui/screens/AlarmAlertScreen.kt | 135 ++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 4 files changed, 164 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/com/bnyro/clock/ui/screens/AlarmAlertScreen.kt 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 @@