Merge branch 'use_alarm_manager' of https://gitea.presley.nz/joseph/Massive into joseph-use_alarm_manager - 2.32 🚀
This commit is contained in:
commit
6d22bee440
|
@ -87,8 +87,8 @@ android {
|
||||||
applicationId "com.massive"
|
applicationId "com.massive"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 36246
|
versionCode 36247
|
||||||
versionName "2.31"
|
versionName "2.32"
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
release {
|
release {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.ACCESS_NETWORK_STATE"
|
android:name="android.permission.ACCESS_NETWORK_STATE"
|
||||||
|
|
171
android/app/src/main/java/com/massive/Timer.kt
Normal file
171
android/app/src/main/java/com/massive/Timer.kt
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
package com.massive
|
||||||
|
|
||||||
|
import android.app.AlarmManager
|
||||||
|
import android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
class Timer(private var msTimerDuration: Long) {
|
||||||
|
|
||||||
|
enum class State {
|
||||||
|
Running,
|
||||||
|
Paused,
|
||||||
|
Expired
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start(context: Context) {
|
||||||
|
if (state != State.Paused) return
|
||||||
|
endTime = SystemClock.elapsedRealtime() + msTimerDuration
|
||||||
|
registerPendingIntent(context)
|
||||||
|
state = State.Running
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop(context: Context) {
|
||||||
|
if (state != State.Running) return
|
||||||
|
msTimerDuration = endTime - SystemClock.elapsedRealtime()
|
||||||
|
unregisterPendingIntent(context)
|
||||||
|
state = State.Paused
|
||||||
|
}
|
||||||
|
|
||||||
|
fun expire() {
|
||||||
|
state = State.Expired
|
||||||
|
msTimerDuration = 0
|
||||||
|
totalTimerDuration = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRemainingSeconds(): Int {
|
||||||
|
return (getRemainingMillis() / 1000).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun increaseDuration(context: Context, milli: Long) {
|
||||||
|
val wasRunning = isRunning()
|
||||||
|
if (wasRunning) stop(context)
|
||||||
|
msTimerDuration += milli
|
||||||
|
totalTimerDuration += milli
|
||||||
|
if (wasRunning) start(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isExpired(): Boolean {
|
||||||
|
return state == State.Expired
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDurationSeconds(): Int {
|
||||||
|
return (totalTimerDuration / 1000).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRemainingMillis(): Long {
|
||||||
|
return if (state == State.Running) endTime - SystemClock.elapsedRealtime()
|
||||||
|
else
|
||||||
|
msTimerDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isRunning(): Boolean {
|
||||||
|
return state == State.Running
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestPermission(context: Context): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true
|
||||||
|
val intent = Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
|
||||||
|
intent.data = Uri.parse("package:" + context.packageName)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
return try {
|
||||||
|
val receiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context2: Context?, intent: Intent?) {
|
||||||
|
context.unregisterReceiver(this)
|
||||||
|
registerPendingIntent(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.registerReceiver(
|
||||||
|
receiver,
|
||||||
|
IntentFilter(AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED),
|
||||||
|
Context.RECEIVER_NOT_EXPORTED
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
context.registerReceiver(
|
||||||
|
receiver,
|
||||||
|
IntentFilter(AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(intent)
|
||||||
|
false
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Request for SCHEDULE_EXACT_ALARM rejected on your device",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun incorrectPermissions(context: Context, alarmManager: AlarmManager): Boolean {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
|
&& !alarmManager.canScheduleExactAlarms()
|
||||||
|
&& !requestPermission(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAlarmManager(context: Context): AlarmManager {
|
||||||
|
return context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterPendingIntent(context: Context) {
|
||||||
|
val intent = Intent(context, TimerService::class.java)
|
||||||
|
.setAction(TimerService.TIMER_EXPIRED)
|
||||||
|
val pendingIntent = PendingIntent.getService(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
val alarmManager = getAlarmManager(context)
|
||||||
|
if (incorrectPermissions(context, alarmManager)) return
|
||||||
|
|
||||||
|
alarmManager.cancel(pendingIntent)
|
||||||
|
pendingIntent.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerPendingIntent(context: Context) {
|
||||||
|
val intent = Intent(context, TimerService::class.java)
|
||||||
|
.setAction(TimerService.TIMER_EXPIRED)
|
||||||
|
val pendingIntent = PendingIntent.getService(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
val alarmManager = getAlarmManager(context)
|
||||||
|
if (incorrectPermissions(context, alarmManager)) return
|
||||||
|
|
||||||
|
alarmManager.setExactAndAllowWhileIdle(
|
||||||
|
ELAPSED_REALTIME_WAKEUP,
|
||||||
|
endTime,
|
||||||
|
pendingIntent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var endTime: Long = 0
|
||||||
|
private var totalTimerDuration: Long = msTimerDuration
|
||||||
|
private var state: State = State.Paused
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun emptyTimer(): Timer {
|
||||||
|
return Timer(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const val ONE_MINUTE_MILLI: Long = 60000
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,8 +28,7 @@ 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 secondsLeft: Int = 0
|
private var timer: Timer = Timer.emptyTimer()
|
||||||
private var secondsTotal: Int = 0
|
|
||||||
private var mediaPlayer: MediaPlayer? = null
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
private var vibrator: Vibrator? = null
|
private var vibrator: Vibrator? = null
|
||||||
private var currentDescription = ""
|
private var currentDescription = ""
|
||||||
|
@ -38,6 +37,10 @@ class TimerService : Service() {
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
Log.d("TimerService", "Received stop broadcast intent")
|
Log.d("TimerService", "Received stop broadcast intent")
|
||||||
|
timer.stop(applicationContext)
|
||||||
|
timer.expire()
|
||||||
|
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,9 +48,8 @@ class TimerService : Service() {
|
||||||
private val addReceiver =
|
private val addReceiver =
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
secondsLeft += 60;
|
timer.increaseDuration(applicationContext, Timer.ONE_MINUTE_MILLI)
|
||||||
secondsTotal += 60;
|
updateNotification(timer.getRemainingSeconds())
|
||||||
updateNotification(secondsLeft)
|
|
||||||
mediaPlayer?.stop()
|
mediaPlayer?.stop()
|
||||||
vibrator?.cancel()
|
vibrator?.cancel()
|
||||||
}
|
}
|
||||||
|
@ -72,46 +74,59 @@ class TimerService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
private fun onTimerStart(intent: Intent?) {
|
||||||
timerRunnable?.let { timerHandler.removeCallbacks(it) }
|
timerRunnable?.let { timerHandler.removeCallbacks(it) }
|
||||||
secondsLeft = (intent?.getIntExtra("milliseconds", 0) ?: 0) / 1000
|
|
||||||
currentDescription = intent?.getStringExtra("description").toString()
|
currentDescription = intent?.getStringExtra("description").toString()
|
||||||
secondsTotal = secondsLeft
|
|
||||||
val startTime = System.currentTimeMillis()
|
timer.stop(applicationContext)
|
||||||
|
timer = Timer((intent?.getIntExtra("milliseconds", 0) ?: 0).toLong())
|
||||||
|
timer.start(applicationContext)
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
startForeground(ONGOING_ID, getProgress(secondsLeft).build(), FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
startForeground(
|
||||||
} else
|
ONGOING_ID,
|
||||||
{
|
getProgress(timer.getRemainingSeconds()).build(),
|
||||||
startForeground(ONGOING_ID, getProgress(secondsLeft).build())
|
FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
startForeground(ONGOING_ID, getProgress(timer.getRemainingSeconds()).build())
|
||||||
}
|
}
|
||||||
|
|
||||||
battery()
|
battery()
|
||||||
Log.d("TimerService", "onStartCommand seconds=$secondsTotal")
|
Log.d("TimerService", "onTimerStart seconds=${timer.getDurationSeconds()}")
|
||||||
|
|
||||||
timerRunnable = object : Runnable {
|
timerRunnable = object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
val millisElapsed = System.currentTimeMillis() - startTime
|
val startTime = SystemClock.elapsedRealtime()
|
||||||
val secondsElapsed = (millisElapsed / 1000).toInt()
|
if (timer.isExpired()) return
|
||||||
if (secondsElapsed < secondsTotal) {
|
updateNotification(timer.getRemainingSeconds())
|
||||||
secondsLeft = secondsTotal - secondsElapsed
|
|
||||||
updateNotification(secondsLeft)
|
val delay = timer.getRemainingMillis() % 1000
|
||||||
timerHandler.postDelayed(this, 1000 - millisElapsed % 1000)
|
timerHandler.postDelayed(this, if (SystemClock.elapsedRealtime() - startTime + delay > 980) 20 else delay)
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
timerHandler.postDelayed(timerRunnable!!, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onTimerExpired() {
|
||||||
|
Log.d("TimerService", "onTimerExpired duration=${timer.getDurationSeconds()}")
|
||||||
|
timer.expire()
|
||||||
|
|
||||||
val settings = getSettings()
|
val settings = getSettings()
|
||||||
vibrate(settings)
|
vibrate(settings)
|
||||||
playSound(settings)
|
playSound(settings)
|
||||||
notifyFinished()
|
notifyFinished()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
timerHandler.postDelayed(timerRunnable!!, 1000)
|
if (intent != null && intent.action == TIMER_EXPIRED) onTimerExpired()
|
||||||
|
else onTimerStart(intent)
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
timerHandler.removeCallbacks(timerRunnable!!)
|
timerRunnable?.let { timerHandler.removeCallbacks(it) }
|
||||||
applicationContext.unregisterReceiver(stopReceiver)
|
applicationContext.unregisterReceiver(stopReceiver)
|
||||||
applicationContext.unregisterReceiver(addReceiver)
|
applicationContext.unregisterReceiver(addReceiver)
|
||||||
mediaPlayer?.stop()
|
mediaPlayer?.stop()
|
||||||
|
@ -217,7 +232,7 @@ class TimerService : Service() {
|
||||||
.setContentTitle(currentDescription)
|
.setContentTitle(currentDescription)
|
||||||
.setContentText(notificationText)
|
.setContentText(notificationText)
|
||||||
.setSmallIcon(R.drawable.ic_baseline_timer_24)
|
.setSmallIcon(R.drawable.ic_baseline_timer_24)
|
||||||
.setProgress(secondsTotal, timeLeftInSeconds, false)
|
.setProgress(timer.getDurationSeconds(), timeLeftInSeconds, false)
|
||||||
.setContentIntent(contentPending)
|
.setContentIntent(contentPending)
|
||||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||||
.setAutoCancel(false)
|
.setAutoCancel(false)
|
||||||
|
@ -261,7 +276,11 @@ class TimerService : Service() {
|
||||||
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) {
|
||||||
val channel =
|
val channel =
|
||||||
NotificationChannel(channelId, "Timer Finished Channel", NotificationManager.IMPORTANCE_HIGH)
|
NotificationChannel(
|
||||||
|
channelId,
|
||||||
|
"Timer Finished Channel",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
)
|
||||||
channel.setSound(null, null)
|
channel.setSound(null, null)
|
||||||
channel.setBypassDnd(true)
|
channel.setBypassDnd(true)
|
||||||
channel.enableVibration(false)
|
channel.enableVibration(false)
|
||||||
|
@ -332,6 +351,7 @@ class TimerService : Service() {
|
||||||
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 ADD_BROADCAST = "add-timer-event"
|
||||||
|
const val TIMER_EXPIRED = "timer-expired-event"
|
||||||
const val ONGOING_ID = 1
|
const val ONGOING_ID = 1
|
||||||
const val FINISHED_ID = 1
|
const val FINISHED_ID = 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "massive",
|
"name": "massive",
|
||||||
"version": "2.31",
|
"version": "2.32",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user