Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add alarm manager #1

Merged
merged 7 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 0 additions & 3 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@ android {
}
}
buildTypes {
debug {
signingConfig signingConfigs.release
}
release {
signingConfig signingConfigs.release
}
Expand Down
30 changes: 19 additions & 11 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />

<application
android:label="Flexify"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:label="Flexify">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
Expand All @@ -38,6 +39,13 @@
android:value="2" />

<activity android:name=".StopAlarm" />
<service android:name=".TimerService" />
<service
android:name=".TimerService"
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="App does not require SCHEDULE_EXACT_ALARM or USE_EXACT_ALARM, but needs foreground service for foreground timer." />
</service>
</application>
</manifest>
190 changes: 190 additions & 0 deletions android/app/src/main/kotlin/com/presley/flexify/FlexifyTimer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package com.presley.flexify

import android.app.AlarmManager
import android.app.AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED
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 FlexifyTimer(private var msTimerDuration: Long) {

enum class State {
Running,
Paused,
Expired
}

fun start(context: Context, elapsedTime: Long = 0) {
if (state != State.Paused) return
msTimerDuration -= elapsedTime
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 isRunning(): Boolean {
return state == State.Running
}

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
}

fun generateMethodChannelPayload(): LongArray {
return longArrayOf(
totalTimerDuration,
totalTimerDuration - getRemainingMillis(),
System.currentTimeMillis(),
state.ordinal.toLong()
)
}

fun hasSecondsUpdated(): Boolean {
val remainingSeconds = getRemainingSeconds()
if (previousSeconds == remainingSeconds) return false
previousSeconds = remainingSeconds
return true
}

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(ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED),
Context.RECEIVER_NOT_EXPORTED
)
} else {
context.registerReceiver(
receiver,
IntentFilter(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 previousSeconds: Int = 0
private var state: State = State.Paused
private var totalTimerDuration: Long = msTimerDuration


companion object {
fun emptyTimer(): FlexifyTimer {
return FlexifyTimer(0)
}

const val ONE_MINUTE_MILLI: Long = 60000
}
}
46 changes: 31 additions & 15 deletions android/app/src/main/kotlin/com/presley/flexify/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class MainActivity : FlutterActivity() {
when (call.method) {
"timer" -> {
val args = call.arguments as ArrayList<*>
timer(args[0] as Int, args[1] as String)
timer(args[0] as Int, args[1] as String, args[2] as Long)
}

"save" -> {
Expand All @@ -66,22 +66,23 @@ class MainActivity : FlutterActivity() {
}

"getProgress" -> {
if (timerBound && timerService?.running == true)
if (timerBound && timerService?.flexifyTimer?.isRunning() == true)
result.success(
intArrayOf(
timerService?.secondsLeft!!,
timerService?.secondsTotal!!
timerService?.flexifyTimer!!.getRemainingSeconds(),
timerService?.flexifyTimer!!.getDurationSeconds()
)
)
else result.success(intArrayOf(0, 0))
}

"add" -> {
if (timerService?.running == true) {
if (timerService?.flexifyTimer?.isRunning() == true) {
val intent = Intent(TimerService.ADD_BROADCAST)
sendBroadcast(intent)
} else {
timer(1000 * 60, "Rest timer")
val args = call.arguments as ArrayList<*>
timer(1000 * 60, "Rest timer", args[0] as Long)
}
}

Expand All @@ -95,27 +96,41 @@ class MainActivity : FlutterActivity() {
}
}
}
applicationContext.registerReceiver(
tickReceiver, IntentFilter(TICK_BROADCAST),
RECEIVER_VISIBLE_TO_INSTANT_APPS
)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
applicationContext.registerReceiver(
tickReceiver, IntentFilter(TICK_BROADCAST),
RECEIVER_NOT_EXPORTED
)
} else {
applicationContext.registerReceiver(tickReceiver, IntentFilter(TICK_BROADCAST))
}
}

private val tickReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val secondsLeft = intent.getIntExtra("secondsLeft", 0)
val secondsTotal = intent.getIntExtra("secondsTotal", 1)
channel?.invokeMethod("tick", intArrayOf(secondsLeft, secondsTotal))
channel?.invokeMethod(
"tick",
timerService?.flexifyTimer?.generateMethodChannelPayload()
)
}
}

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
timerService?.apply {
mainActivityVisible = hasFocus
updateTimerNotificationRefreshRate()
}
}

override fun onDestroy() {
super.onDestroy()
applicationContext.unregisterReceiver(tickReceiver)
}

private fun timer(milliseconds: Int, description: String) {
private fun timer(milliseconds: Int, description: String, timeStamp: Long) {
Log.d("MainActivity", "Queue $description for $milliseconds delay")
val intent = Intent(context, TimerService::class.java).also { intent ->
bindService(
Expand All @@ -126,6 +141,7 @@ class MainActivity : FlutterActivity() {
}
intent.putExtra("milliseconds", milliseconds)
intent.putExtra("description", description)
intent.putExtra("timeStamp", timeStamp)
context.startForegroundService(intent)
}

Expand Down Expand Up @@ -203,7 +219,7 @@ class MainActivity : FlutterActivity() {

override fun onResume() {
super.onResume()
if (timerService?.running != true) {
if (timerService?.flexifyTimer?.isRunning() != true) {
val intent = Intent(TimerService.STOP_BROADCAST)
sendBroadcast(intent);
}
Expand Down