Fix dismissing alarm and add +1 minute button

This commit is contained in:
Brandon Presley 2024-02-16 17:47:26 +13:00
parent 5355b0eb6a
commit 47cfaa4b67
6 changed files with 174 additions and 461 deletions

View File

@ -11,43 +11,40 @@
android:name="android.permission.ACCESS_NETWORK_STATE" android:name="android.permission.ACCESS_NETWORK_STATE"
tools:node="remove" /> tools:node="remove" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" <uses-permission
tools:node="remove"/> android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
android:label="@string/app_name" android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/ic_launcher" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask" android:exported="true"
android:windowSoftInputMode="adjustResize" android:launchMode="singleTask"
android:exported="true"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".TimerDone" android:name=".TimerDone"
android:exported="false"> android:exported="false">
<meta-data <meta-data
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="" /> android:value="" />
</activity> </activity>
<activity <activity
android:name=".StopAlarm" android:name=".StopAlarm"
android:exported="true" android:exported="true"
android:process=":remote" /> android:process=":remote" />
<service
android:name=".AlarmService"
android:exported="false" />
<service <service
android:name=".TimerService" android:name=".TimerService"

View File

@ -1,217 +1,25 @@
package com.massive package com.massive
import android.annotation.SuppressLint
import android.app.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.os.CountDownTimer
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.annotation.RequiresApi
import com.facebook.react.bridge.* import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlin.math.floor
@SuppressLint("UnspecifiedRegisterReceiverFlag") @RequiresApi(Build.VERSION_CODES.O)
class AlarmModule(context: ReactApplicationContext?) : class AlarmModule(context: ReactApplicationContext?) :
ReactContextBaseJavaModule(context), LifecycleEventListener { ReactContextBaseJavaModule(context) {
private var countdownTimer: CountDownTimer? = null
var currentMs: Long = 0
private var running = false
private var currentDescription = ""
override fun getName(): String { override fun getName(): String {
return "AlarmModule" return "AlarmModule"
} }
private val stopReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d("AlarmModule", "Received stop broadcast intent")
stop()
}
}
private val addReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
add()
}
}
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST),
Context.RECEIVER_NOT_EXPORTED)
reactApplicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST),
Context.RECEIVER_NOT_EXPORTED)
}
else {
reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST))
reactApplicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST))
}
}
override fun onHostDestroy() {
reactApplicationContext.unregisterReceiver(stopReceiver)
reactApplicationContext.unregisterReceiver(addReceiver)
}
@ReactMethod
fun add() {
Log.d("AlarmModule", "Add 1 min to alarm.")
countdownTimer?.cancel()
val newMs = if (running) currentMs.toInt().plus(60000) else 60000
countdownTimer = getTimer(newMs)
countdownTimer?.start()
running = true
val manager = getManager()
manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
val intent = Intent(reactApplicationContext, AlarmService::class.java)
reactApplicationContext.stopService(intent)
}
@ReactMethod(isBlockingSynchronousMethod = true)
fun getCurrent(): Int {
Log.d("AlarmModule", "currentMs=$currentMs")
if (running) return currentMs.toInt()
return 0
}
@ReactMethod
fun stop() {
Log.d("AlarmModule", "Stop alarm.")
countdownTimer?.cancel()
running = false
val intent = Intent(reactApplicationContext, AlarmService::class.java)
reactApplicationContext?.stopService(intent)
val manager = getManager()
manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
manager.cancel(NOTIFICATION_ID_PENDING)
val params =
Arguments.createMap().apply {
putString("minutes", "00")
putString("seconds", "00")
}
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("tick", params)
}
@ReactMethod @ReactMethod
fun timer(milliseconds: Int, description: String) { fun timer(milliseconds: Int, description: String) {
Log.d("AlarmModule", "Queue alarm for $milliseconds delay") Log.d("AlarmModule", "Queue alarm for $milliseconds delay")
val intent = Intent(reactApplicationContext, TimerService::class.java) val intent = Intent(reactApplicationContext, TimerService::class.java)
intent.putExtra("milliseconds", milliseconds) intent.putExtra("milliseconds", milliseconds)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { intent.putExtra("description", description)
reactApplicationContext.startForegroundService(intent) reactApplicationContext.startForegroundService(intent)
}
else {
reactApplicationContext.startService(intent)
}
}
private fun getTimer(
endMs: Int,
): CountDownTimer {
val builder = getBuilder()
return object : CountDownTimer(endMs.toLong(), 1000) {
override fun onTick(current: Long) {
currentMs = current
val seconds =
floor((current / 1000).toDouble() % 60).toInt().toString().padStart(2, '0')
val minutes =
floor((current / 1000).toDouble() / 60).toInt().toString().padStart(2, '0')
builder.setContentText("$minutes:$seconds")
.setAutoCancel(false)
.setDefaults(0)
.setProgress(endMs, current.toInt(), false)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.priority = NotificationCompat.PRIORITY_LOW
val manager = getManager()
manager.notify(NOTIFICATION_ID_PENDING, builder.build())
val params =
Arguments.createMap().apply {
putString("minutes", minutes)
putString("seconds", seconds)
}
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("tick", params)
}
override fun onFinish() {
val context = reactApplicationContext
val intent = Intent(context, AlarmService::class.java)
context.startService(intent)
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(
"tick",
Arguments.createMap().apply {
putString("minutes", "00")
putString("seconds", "00")
}
)
}
}
}
@SuppressLint("UnspecifiedImmutableFlag")
private fun getBuilder(): NotificationCompat.Builder {
val context = reactApplicationContext
val contentIntent = Intent(context, MainActivity::class.java)
val pendingContent =
PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
val addBroadcast = Intent(ADD_BROADCAST).apply { setPackage(context.packageName) }
val pendingAdd =
PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE)
val stopBroadcast = Intent(STOP_BROADCAST)
stopBroadcast.setPackage(context.packageName)
val pendingStop =
PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(context, CHANNEL_ID_PENDING)
.setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24)
.setContentTitle(currentDescription)
.setContentIntent(pendingContent)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
.setDeleteIntent(pendingStop)
}
private fun getManager(): NotificationManager {
val notificationManager =
reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val timersChannel =
NotificationChannel(
CHANNEL_ID_PENDING,
CHANNEL_ID_PENDING,
NotificationManager.IMPORTANCE_LOW
)
timersChannel.setSound(null, null)
timersChannel.description = "Progress on rest timers."
notificationManager.createNotificationChannel(timersChannel)
}
return notificationManager
}
companion object {
const val STOP_BROADCAST = "stop-timer-event"
const val ADD_BROADCAST = "add-timer-event"
const val CHANNEL_ID_PENDING = "Timer"
const val NOTIFICATION_ID_PENDING = 1
}
override fun onHostResume() {
TODO("Not yet implemented")
}
override fun onHostPause() {
TODO("Not yet implemented")
} }
} }

View File

@ -1,158 +0,0 @@
package com.massive
import android.annotation.SuppressLint
import android.app.*
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.media.MediaPlayer.OnPreparedListener
import android.net.Uri
import android.os.*
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean, val duration: Long)
class AlarmService : Service(), OnPreparedListener {
private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null
private fun getBuilder(): NotificationCompat.Builder {
val context = applicationContext
val contentIntent = Intent(context, MainActivity::class.java)
val pendingContent =
PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
val addBroadcast = Intent(AlarmModule.ADD_BROADCAST).apply {
setPackage(context.packageName)
}
val pendingAdd =
PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE)
val stopBroadcast = Intent(AlarmModule.STOP_BROADCAST)
stopBroadcast.setPackage(context.packageName)
val pendingStop =
PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(context, AlarmModule.CHANNEL_ID_PENDING)
.setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting")
.setSound(null)
.setContentIntent(pendingContent)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
.setDeleteIntent(pendingStop)
}
@SuppressLint("Range")
private fun getSettings(): Settings {
val db = DatabaseHelper(applicationContext).readableDatabase
val cursor = db.rawQuery("SELECT sound, noSound, vibrate, duration FROM settings", null)
cursor.moveToFirst()
val sound = cursor.getString(cursor.getColumnIndex("sound"))
val noSound = cursor.getInt(cursor.getColumnIndex("noSound")) == 1
val vibrate = cursor.getInt(cursor.getColumnIndex("vibrate")) == 1
var duration = cursor.getLong(cursor.getColumnIndex("duration"))
if (duration.toInt() == 0) duration = 300
cursor.close()
return Settings(sound, noSound, vibrate, duration)
}
private fun playSound(settings: Settings) {
if (settings.noSound) return
if (settings.sound == null) {
mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon)
mediaPlayer?.start()
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }
} else {
mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setDataSource(applicationContext, Uri.parse(settings.sound))
prepare()
start()
setOnCompletionListener { vibrator?.cancel() }
}
}
}
private fun doNotify(): Notification {
val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val alarmsChannel = NotificationChannel(
CHANNEL_ID_DONE,
CHANNEL_ID_DONE,
NotificationManager.IMPORTANCE_HIGH
)
alarmsChannel.description = "Alarms for rest timers."
alarmsChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
alarmsChannel.setSound(null, null)
manager.createNotificationChannel(alarmsChannel)
}
val builder = getBuilder()
val context = applicationContext
val finishIntent = Intent(context, StopAlarm::class.java)
val finishPending = PendingIntent.getActivity(
context, 0, finishIntent, PendingIntent.FLAG_IMMUTABLE
)
val fullIntent = Intent(context, TimerDone::class.java)
val fullPending = PendingIntent.getActivity(
context, 0, fullIntent, PendingIntent.FLAG_IMMUTABLE
)
builder.setContentText("Timer finished.").setProgress(0, 0, false)
.setAutoCancel(true).setOngoing(true).setFullScreenIntent(fullPending, true)
.setContentIntent(finishPending).setChannelId(CHANNEL_ID_DONE)
.setCategory(NotificationCompat.CATEGORY_ALARM).priority =
NotificationCompat.PRIORITY_HIGH
val notification = builder.build()
manager.notify(NOTIFICATION_ID_DONE, notification)
manager.cancel(AlarmModule.NOTIFICATION_ID_PENDING)
return notification
}
@RequiresApi(Build.VERSION_CODES.O)
@SuppressLint("Recycle")
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
doNotify()
val settings = getSettings()
playSound(settings)
if (!settings.vibrate) return START_STICKY
val pattern = longArrayOf(0, settings.duration, 1000, settings.duration, 1000, settings.duration)
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(VIBRATOR_SERVICE) as Vibrator
}
vibrator!!.vibrate(VibrationEffect.createWaveform(pattern, -1))
val handler = Handler(Looper.getMainLooper())
handler.postDelayed({ vibrator!!.cancel() }, 10000)
return START_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onPrepared(player: MediaPlayer) {
player.start()
}
override fun onDestroy() {
super.onDestroy()
mediaPlayer?.stop()
mediaPlayer?.release()
vibrator?.cancel()
}
companion object {
const val CHANNEL_ID_DONE = "Alarm"
const val NOTIFICATION_ID_DONE = 2
}
}

View File

@ -1,17 +1,11 @@
package com.massive package com.massive
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.View
import android.view.WindowManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.massive.AlarmService
import com.massive.MainActivity
class StopAlarm : Activity() { class StopAlarm : Activity() {
@RequiresApi(Build.VERSION_CODES.O_MR1) @RequiresApi(Build.VERSION_CODES.O_MR1)
@ -19,7 +13,7 @@ class StopAlarm : Activity() {
Log.d("AlarmActivity", "Call to AlarmActivity") Log.d("AlarmActivity", "Call to AlarmActivity")
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val context = applicationContext val context = applicationContext
context.stopService(Intent(context, AlarmService::class.java)) context.stopService(Intent(context, TimerService::class.java))
savedInstanceState.apply { setShowWhenLocked(true) } savedInstanceState.apply { setShowWhenLocked(true) }
val intent = Intent(context, MainActivity::class.java) val intent = Intent(context, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

View File

@ -1,8 +1,5 @@
package com.massive package com.massive
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -10,6 +7,7 @@ import android.util.Log
import android.view.View import android.view.View
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationManagerCompat
class TimerDone : AppCompatActivity() { class TimerDone : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -21,38 +19,11 @@ class TimerDone : AppCompatActivity() {
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun stop(view: View) { fun stop(view: View) {
Log.d("TimerDone", "Stopping...") Log.d("TimerDone", "Stopping...")
applicationContext.stopService(Intent(applicationContext, AlarmService::class.java)) applicationContext.stopService(Intent(applicationContext, TimerService::class.java))
val manager = getManager() val manager = NotificationManagerCompat.from(this)
manager.cancel(AlarmService.NOTIFICATION_ID_DONE) manager.cancel(TimerService.CHANNEL_ID)
manager.cancel(AlarmModule.NOTIFICATION_ID_PENDING)
val intent = Intent(applicationContext, MainActivity::class.java) val intent = Intent(applicationContext, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
applicationContext.startActivity(intent) applicationContext.startActivity(intent)
} }
@RequiresApi(Build.VERSION_CODES.O)
fun getManager(): NotificationManager {
val alarmsChannel = NotificationChannel(
AlarmService.CHANNEL_ID_DONE,
AlarmService.CHANNEL_ID_DONE,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Alarms for rest timers."
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
}
val timersChannel = NotificationChannel(
AlarmModule.CHANNEL_ID_PENDING,
AlarmModule.CHANNEL_ID_PENDING,
NotificationManager.IMPORTANCE_LOW
).apply {
setSound(null, null)
description = "Progress on rest timers."
}
val notificationManager = applicationContext.getSystemService(
NotificationManager::class.java
)
notificationManager.createNotificationChannel(alarmsChannel)
notificationManager.createNotificationChannel(timersChannel)
return notificationManager
}
} }

View File

@ -8,19 +8,27 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.net.Uri
import android.os.* import android.os.*
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean, val duration: Long)
@RequiresApi(Build.VERSION_CODES.O)
class TimerService : Service() { class TimerService : Service() {
private lateinit var timerHandler: Handler private lateinit var timerHandler: Handler
private var timerRunnable: Runnable? = null private var timerRunnable: Runnable? = null
private var timeLeftInSeconds: Int = 0 private var secondsLeft: Int = 0
private var timeTotalInSeconds: Int = 0 private var secondsTotal: Int = 0
private var notificationId = 1 private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null
private var currentDescription = ""
private val stopReceiver = private val stopReceiver =
object : BroadcastReceiver() { object : BroadcastReceiver() {
@ -30,6 +38,16 @@ class TimerService : Service() {
} }
} }
private val addReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
secondsLeft += 60;
secondsTotal += 60;
mediaPlayer?.stop()
vibrator?.cancel()
}
}
@SuppressLint("UnspecifiedRegisterReceiverFlag") @SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -37,27 +55,33 @@ class TimerService : Service() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
applicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST), applicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST),
Context.RECEIVER_NOT_EXPORTED) Context.RECEIVER_NOT_EXPORTED)
applicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST),
Context.RECEIVER_NOT_EXPORTED)
} }
else { else {
applicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST)) applicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST))
applicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST))
} }
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
timeLeftInSeconds = (intent?.getIntExtra("milliseconds", 0) ?: 0) / 1000 secondsLeft = (intent?.getIntExtra("milliseconds", 0) ?: 0) / 1000
startForeground(notificationId, createNotification(timeLeftInSeconds)) currentDescription = intent?.getStringExtra("description").toString()
Log.d("TimerService", "onStartCommand seconds=$timeLeftInSeconds") secondsTotal = secondsLeft
timeTotalInSeconds = timeLeftInSeconds startForeground(CHANNEL_ID, getProgress(secondsLeft).build())
Log.d("TimerService", "onStartCommand seconds=$secondsLeft")
timerRunnable = object : Runnable { timerRunnable = object : Runnable {
override fun run() { override fun run() {
if (timeLeftInSeconds > 0) { if (secondsLeft > 0) {
timeLeftInSeconds-- secondsLeft--
updateNotification(timeLeftInSeconds) updateNotification(secondsLeft)
timerHandler.postDelayed(this, 1000) timerHandler.postDelayed(this, 1000)
} else { } else {
startAlarmService() val settings = getSettings()
stopSelf() vibrate(settings)
playSound(settings)
notifyFinished()
} }
} }
} }
@ -69,14 +93,53 @@ class TimerService : Service() {
super.onDestroy() super.onDestroy()
timerHandler.removeCallbacks(timerRunnable!!) timerHandler.removeCallbacks(timerRunnable!!)
applicationContext.unregisterReceiver(stopReceiver) applicationContext.unregisterReceiver(stopReceiver)
applicationContext.unregisterReceiver(addReceiver)
mediaPlayer?.stop()
mediaPlayer?.release()
vibrator?.cancel()
} }
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
return null return null
} }
private fun createNotification(timeLeftInSeconds: Int): Notification { @SuppressLint("Range")
val notificationTitle = "Timer" private fun getSettings(): Settings {
val db = DatabaseHelper(applicationContext).readableDatabase
val cursor = db.rawQuery("SELECT sound, noSound, vibrate, duration FROM settings", null)
cursor.moveToFirst()
val sound = cursor.getString(cursor.getColumnIndex("sound"))
val noSound = cursor.getInt(cursor.getColumnIndex("noSound")) == 1
val vibrate = cursor.getInt(cursor.getColumnIndex("vibrate")) == 1
var duration = cursor.getLong(cursor.getColumnIndex("duration"))
if (duration.toInt() == 0) duration = 300
cursor.close()
return Settings(sound, noSound, vibrate, duration)
}
private fun playSound(settings: Settings) {
if (settings.noSound) return
if (settings.sound == null) {
mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon)
mediaPlayer?.start()
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }
} else {
mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setDataSource(applicationContext, Uri.parse(settings.sound))
prepare()
start()
setOnCompletionListener { vibrator?.cancel() }
}
}
}
private fun getProgress(timeLeftInSeconds: Int): NotificationCompat.Builder {
val notificationText = formatTime(timeLeftInSeconds) val notificationText = formatTime(timeLeftInSeconds)
val notificationChannelId = "timer_channel" val notificationChannelId = "timer_channel"
val notificationIntent = Intent(this, TimerService::class.java) val notificationIntent = Intent(this, TimerService::class.java)
@ -86,16 +149,19 @@ class TimerService : Service() {
notificationIntent, notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
) )
val stopBroadcast = Intent(AlarmModule.STOP_BROADCAST) val stopBroadcast = Intent(STOP_BROADCAST)
stopBroadcast.setPackage(applicationContext.packageName) stopBroadcast.setPackage(applicationContext.packageName)
val pendingStop = val pendingStop =
PendingIntent.getBroadcast(applicationContext, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE) PendingIntent.getBroadcast(applicationContext, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
val addBroadcast = Intent(ADD_BROADCAST).apply { setPackage(applicationContext.packageName) }
val pendingAdd =
PendingIntent.getBroadcast(applicationContext, 0, addBroadcast, PendingIntent.FLAG_MUTABLE)
val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId) val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId)
.setContentTitle(notificationTitle) .setContentTitle(currentDescription)
.setContentText(notificationText) .setContentText(notificationText)
.setSmallIcon(R.drawable.ic_baseline_timer_24) .setSmallIcon(R.drawable.ic_baseline_timer_24)
.setProgress(timeTotalInSeconds, timeLeftInSeconds, false) .setProgress(secondsTotal, timeLeftInSeconds, false)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setCategory(NotificationCompat.CATEGORY_PROGRESS) .setCategory(NotificationCompat.CATEGORY_PROGRESS)
.setAutoCancel(false) .setAutoCancel(false)
@ -104,6 +170,7 @@ class TimerService : Service() {
.setOngoing(true) .setOngoing(true)
.setDeleteIntent(pendingStop) .setDeleteIntent(pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop) .addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
val notificationManager = NotificationManagerCompat.from(this) val notificationManager = NotificationManagerCompat.from(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -115,27 +182,64 @@ class TimerService : Service() {
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
return notificationBuilder.build() return notificationBuilder
} }
private fun updateNotification(timeLeftInSeconds: Int) { private fun vibrate(settings: Settings) {
if (!settings.vibrate) return
val pattern = longArrayOf(0, settings.duration, 1000, settings.duration, 1000, settings.duration)
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(VIBRATOR_SERVICE) as Vibrator
}
vibrator!!.vibrate(VibrationEffect.createWaveform(pattern, -1))
val handler = Handler(Looper.getMainLooper())
handler.postDelayed({ vibrator!!.cancel() }, 10000)
}
private fun notifyFinished() {
val builder = getProgress(0)
val fullIntent = Intent(applicationContext, TimerDone::class.java)
val fullPending = PendingIntent.getActivity(
applicationContext, 0, fullIntent, PendingIntent.FLAG_IMMUTABLE
)
val finishIntent = Intent(applicationContext, StopAlarm::class.java)
val finishPending = PendingIntent.getActivity(
applicationContext, 0, finishIntent, PendingIntent.FLAG_IMMUTABLE
)
builder.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(finishPending)
.setFullScreenIntent(fullPending, true)
.setOngoing(false)
.setProgress(0, 0, false)
val notificationManager = NotificationManagerCompat.from(this) val notificationManager = NotificationManagerCompat.from(this)
val notification = createNotification(timeLeftInSeconds)
if (ActivityCompat.checkSelfPermission( if (ActivityCompat.checkSelfPermission(
this, this,
Manifest.permission.POST_NOTIFICATIONS Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED ) != PackageManager.PERMISSION_GRANTED
) { ) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return return
} }
notificationManager.notify(notificationId, notification) notificationManager.notify(CHANNEL_ID, builder.build())
}
private fun updateNotification(seconds: Int) {
val notificationManager = NotificationManagerCompat.from(this)
val notification = getProgress(seconds)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
notificationManager.notify(CHANNEL_ID, notification.build())
} }
private fun formatTime(timeInSeconds: Int): String { private fun formatTime(timeInSeconds: Int): String {
@ -144,12 +248,9 @@ class TimerService : Service() {
return String.format("%02d:%02d", minutes, seconds) return String.format("%02d:%02d", minutes, seconds)
} }
private fun startAlarmService() {
val intent = Intent(applicationContext, AlarmService::class.java)
applicationContext.startService(intent)
}
companion object { companion object {
const val STOP_BROADCAST = "stop-timer-event" const val STOP_BROADCAST = "stop-timer-event"
const val ADD_BROADCAST = "add-timer-event"
const val CHANNEL_ID = 1
} }
} }