diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8d2759c..6f7d651 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -48,5 +48,9 @@ + + diff --git a/android/app/src/main/java/com/massive/AlarmModule.kt b/android/app/src/main/java/com/massive/AlarmModule.kt index 8b252be..b405bfb 100644 --- a/android/app/src/main/java/com/massive/AlarmModule.kt +++ b/android/app/src/main/java/com/massive/AlarmModule.kt @@ -104,15 +104,14 @@ class AlarmModule(context: ReactApplicationContext?) : @ReactMethod fun timer(milliseconds: Int, description: String) { Log.d("AlarmModule", "Queue alarm for $milliseconds delay") - currentDescription = description - val manager = getManager() - manager.cancel(AlarmService.NOTIFICATION_ID_DONE) - val intent = Intent(reactApplicationContext, AlarmService::class.java) - reactApplicationContext.stopService(intent) - countdownTimer?.cancel() - countdownTimer = getTimer(milliseconds) - countdownTimer?.start() - running = true + val intent = Intent(reactApplicationContext, TimerService::class.java) + intent.putExtra("milliseconds", milliseconds) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + reactApplicationContext.startForegroundService(intent) + } + else { + reactApplicationContext.startService(intent) + } } private fun getTimer( @@ -147,12 +146,7 @@ class AlarmModule(context: ReactApplicationContext?) : override fun onFinish() { val context = reactApplicationContext val intent = Intent(context, AlarmService::class.java) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent) - } - else { - context.startService(intent) - } + context.startService(intent) context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit( "tick", diff --git a/android/app/src/main/java/com/massive/TimerService.kt b/android/app/src/main/java/com/massive/TimerService.kt new file mode 100644 index 0000000..71207fb --- /dev/null +++ b/android/app/src/main/java/com/massive/TimerService.kt @@ -0,0 +1,155 @@ +package com.massive + +import android.Manifest +import android.annotation.SuppressLint +import android.app.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.os.* +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat + +class TimerService : Service() { + + private lateinit var timerHandler: Handler + private var timerRunnable: Runnable? = null + private var timeLeftInSeconds: Int = 0 + private var timeTotalInSeconds: Int = 0 + private var notificationId = 1 + + private val stopReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + Log.d("TimerService", "Received stop broadcast intent") + stopSelf() + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + override fun onCreate() { + super.onCreate() + timerHandler = Handler(Looper.getMainLooper()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + applicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST), + Context.RECEIVER_NOT_EXPORTED) + } + else { + applicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST)) + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + timeLeftInSeconds = (intent?.getIntExtra("milliseconds", 0) ?: 0) / 1000 + startForeground(notificationId, createNotification(timeLeftInSeconds)) + Log.d("TimerService", "onStartCommand seconds=$timeLeftInSeconds") + timeTotalInSeconds = timeLeftInSeconds + + timerRunnable = object : Runnable { + override fun run() { + if (timeLeftInSeconds > 0) { + timeLeftInSeconds-- + updateNotification(timeLeftInSeconds) + timerHandler.postDelayed(this, 1000) + } else { + startAlarmService() + stopSelf() + } + } + } + timerHandler.postDelayed(timerRunnable!!, 1000) + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + timerHandler.removeCallbacks(timerRunnable!!) + applicationContext.unregisterReceiver(stopReceiver) + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + private fun createNotification(timeLeftInSeconds: Int): Notification { + val notificationTitle = "Timer" + val notificationText = formatTime(timeLeftInSeconds) + val notificationChannelId = "timer_channel" + val notificationIntent = Intent(this, TimerService::class.java) + val pendingIntent = PendingIntent.getActivity( + this, + 0, + notificationIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val stopBroadcast = Intent(AlarmModule.STOP_BROADCAST) + stopBroadcast.setPackage(applicationContext.packageName) + val pendingStop = + PendingIntent.getBroadcast(applicationContext, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE) + + val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId) + .setContentTitle(notificationTitle) + .setContentText(notificationText) + .setSmallIcon(R.drawable.ic_baseline_timer_24) + .setProgress(timeTotalInSeconds, timeLeftInSeconds, false) + .setContentIntent(pendingIntent) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setAutoCancel(false) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setOngoing(true) + .setDeleteIntent(pendingStop) + .addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop) + + val notificationManager = NotificationManagerCompat.from(this) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + notificationChannelId, + "Timer Channel", + NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel(channel) + } + + return notificationBuilder.build() + } + + private fun updateNotification(timeLeftInSeconds: Int) { + val notificationManager = NotificationManagerCompat.from(this) + val notification = createNotification(timeLeftInSeconds) + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) != 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 + } + notificationManager.notify(notificationId, notification) + } + + private fun formatTime(timeInSeconds: Int): String { + val minutes = timeInSeconds / 60 + val seconds = timeInSeconds % 60 + return String.format("%02d:%02d", minutes, seconds) + } + + private fun startAlarmService() { + val intent = Intent(applicationContext, AlarmService::class.java) + applicationContext.startService(intent) + } + + companion object { + const val STOP_BROADCAST = "stop-timer-event" + } +}