Skip to content

Commit

Permalink
Switched to periodic timers
Browse files Browse the repository at this point in the history
Dart code has its own Timer class to keep track of the timer
Kotlin code now uses fixedRateTimer, so we no longer need to do any
delay gymnastics

TICK_BROADCASTS are only sent in timer change of states
  • Loading branch information
josephnglynn committed Mar 10, 2024
1 parent f641ff2 commit e8ad86d
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import android.widget.Toast
import androidx.annotation.RequiresApi

@RequiresApi(Build.VERSION_CODES.O)
class Timer(private var msTimerDuration: Long) {
class FlexifyTimer(private var msTimerDuration: Long) {

enum class State {
Running,
Expand Down Expand Up @@ -72,18 +72,12 @@ class Timer(private var msTimerDuration: Long) {
msTimerDuration
}

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

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

Expand Down Expand Up @@ -151,14 +145,13 @@ class Timer(private var msTimerDuration: Long) {
}

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(): Timer {
return Timer(0)
fun emptyTimer(): FlexifyTimer {
return FlexifyTimer(0)
}

const val ONE_MINUTE_MILLI: Long = 60000
Expand Down
20 changes: 6 additions & 14 deletions android/app/src/main/kotlin/com/presley/flexify/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,18 @@ class MainActivity : FlutterActivity() {
}

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

"add" -> {
if (timerService?.timer?.isRunning() == true) {
if (timerService?.flexifyTimer?.isRunning() == true) {
val intent = Intent(TimerService.ADD_BROADCAST)
sendBroadcast(intent)
} else {
Expand All @@ -104,18 +104,10 @@ class MainActivity : FlutterActivity() {
private val tickReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
channel?.invokeMethod("tick", timerService?.timer?.generateMethodChannelPayload())
channel?.invokeMethod("tick", timerService?.flexifyTimer?.generateMethodChannelPayload())
}
}

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

override fun onDestroy() {
super.onDestroy()
applicationContext.unregisterReceiver(tickReceiver)
Expand Down Expand Up @@ -209,7 +201,7 @@ class MainActivity : FlutterActivity() {

override fun onResume() {
super.onResume()
if (timerService?.timer?.isRunning() != true) {
if (timerService?.flexifyTimer?.isRunning() != true) {
val intent = Intent(TimerService.STOP_BROADCAST)
sendBroadcast(intent);
}
Expand Down
94 changes: 36 additions & 58 deletions android/app/src/main/kotlin/com/presley/flexify/TimerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,20 @@ import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import kotlin.math.max
import java.util.Timer
import kotlin.concurrent.fixedRateTimer


@RequiresApi(Build.VERSION_CODES.O)
class TimerService : Service() {

private lateinit var timerHandler: Handler

private var timerRunnable: Runnable? = null
private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null
private val binder = LocalBinder()
private var currentDescription = ""
var timer: Timer = Timer.emptyTimer()
var mainActivityFocused: Boolean = true
private var updateLoopTimer: Timer = Timer();
var flexifyTimer: FlexifyTimer = FlexifyTimer.emptyTimer()


override fun onBind(intent: Intent): IBinder? {
return binder
Expand All @@ -43,24 +42,17 @@ class TimerService : Service() {
fun getService(): TimerService = this@TimerService
}

fun updateTimerUI() {
timerRunnable?.let {
timerHandler.removeCallbacks(it)
timerHandler.postDelayed(it, 0)
}
}

private val stopReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d("TimerService", "Received stop broadcast intent")
timer.stop(applicationContext)
timer.expire()
flexifyTimer.stop(applicationContext)
flexifyTimer.expire()

timerHandler.removeCallbacks(timerRunnable!!)
updateLoopTimer.cancel()
mediaPlayer?.stop()
vibrator?.cancel()
sendTickBroadcast()
updateAppUI()
val notificationManager = NotificationManagerCompat.from(this@TimerService)
notificationManager.cancel(ONGOING_ID)
stopForeground(STOP_FOREGROUND_REMOVE)
Expand All @@ -72,19 +64,20 @@ class TimerService : Service() {
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d("TimerService", "Received add broadcast intent")
if (timer.isExpired()) return startTimer(Timer.ONE_MINUTE_MILLI)
if (flexifyTimer.isExpired()) return startTimer(FlexifyTimer.ONE_MINUTE_MILLI)

timer.increaseDuration(applicationContext, Timer.ONE_MINUTE_MILLI)
updateNotification(timer.getRemainingSeconds())
flexifyTimer.increaseDuration(applicationContext, FlexifyTimer.ONE_MINUTE_MILLI)
updateNotification(flexifyTimer.getRemainingSeconds())
mediaPlayer?.stop()
vibrator?.cancel()
updateAppUI()
}
}

@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),
Expand All @@ -100,63 +93,48 @@ class TimerService : Service() {
}
}

private fun sendTickBroadcast() {
private fun updateAppUI() {
sendBroadcast(Intent(MainActivity.TICK_BROADCAST))
}

private fun onTimerExpired() {
Log.d("TimerService", "onTimerExpired duration=${timer.getDurationSeconds()}")
timer.expire()
Log.d("TimerService", "onTimerExpired duration=${flexifyTimer.getDurationSeconds()}")
flexifyTimer.expire()
vibrate()
playSound()
notifyFinished()
sendTickBroadcast()
updateAppUI()
}

private fun startTimer(msDuration: Long) {
timerRunnable?.let { timerHandler.removeCallbacks(it) }
updateLoopTimer.cancel()

timer.stop(applicationContext)
timer = Timer(msDuration)
timer.start(applicationContext)
flexifyTimer.stop(applicationContext)
flexifyTimer = FlexifyTimer(msDuration)
flexifyTimer.start(applicationContext)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(
ONGOING_ID,
getProgress(timer.getRemainingSeconds()).build(),
getProgress(flexifyTimer.getRemainingSeconds()).build(),
FOREGROUND_SERVICE_TYPE_SPECIAL_USE
)
} else {
startForeground(ONGOING_ID, getProgress(timer.getRemainingSeconds()).build())
startForeground(ONGOING_ID, getProgress(flexifyTimer.getRemainingSeconds()).build())
}

battery()
Log.d("TimerService", "onTimerStart seconds=${timer.getDurationSeconds()}")


timerRunnable?.let { timerHandler.removeCallbacks(it) }
timerRunnable = object : Runnable {
fun scheduleWork(msDelayTime: () -> Long) {
timerHandler.postDelayed(
this,
msDelayTime()
)
}

fun updateUI() {
sendTickBroadcast()
updateNotification(timer.getRemainingSeconds())
}

override fun run() {
if (timer.isExpired()) return
if (timer.hasSecondsUpdated()) updateUI()
if (!mainActivityFocused) return scheduleWork { timer.getRemainingMillis() % 1000 }
val startTime = SystemClock.elapsedRealtime();
scheduleWork { max(0, startTime + 20 - SystemClock.elapsedRealtime()) }
}
Log.d("TimerService", "onTimerStart seconds=${flexifyTimer.getDurationSeconds()}")

updateLoopTimer = fixedRateTimer(
"updateNotificationUI",
false,
flexifyTimer.getRemainingMillis() % 1000,
1000) {
if (!flexifyTimer.isExpired()) updateNotification(flexifyTimer.getRemainingSeconds())
}
timerHandler.postDelayed(timerRunnable!!, 1000)

updateAppUI()
}

private fun onTimerStart(intent: Intent?) {
Expand All @@ -172,7 +150,7 @@ class TimerService : Service() {

override fun onDestroy() {
super.onDestroy()
timerHandler.removeCallbacks(timerRunnable!!)
updateLoopTimer.cancel()
applicationContext.unregisterReceiver(stopReceiver)
applicationContext.unregisterReceiver(addReceiver)
mediaPlayer?.stop()
Expand Down Expand Up @@ -243,7 +221,7 @@ class TimerService : Service() {
.setContentTitle(currentDescription)
.setContentText(formatTime(timeLeftInSeconds))
.setSmallIcon(R.drawable.baseline_timer_24)
.setProgress(timer.getDurationSeconds(), timeLeftInSeconds, false)
.setProgress(flexifyTimer.getDurationSeconds(), timeLeftInSeconds, false)
.setContentIntent(contentPending)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.setAutoCancel(false)
Expand Down
52 changes: 37 additions & 15 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:flexify/timer.dart';
import 'dart:async';

import 'package:flexify/native_timer_wrapper.dart';
import 'package:flexify/database.dart';
import 'package:flexify/graphs_page.dart';
import 'package:flutter/material.dart';
Expand All @@ -11,16 +13,34 @@ late AppDatabase database;
late MethodChannel android;

class AppState extends ChangeNotifier {
Timer? _timer;
String? selectedExercise;
Timer timer = Timer.emptyTimer();
NativeTimerWrapper nativeTimer = NativeTimerWrapper.emptyTimer();

void selectExercise(String exercise) {
selectedExercise = exercise;
notifyListeners();
}

void updateTimer(Timer newTimer) {
timer = newTimer;
void updateTimer(NativeTimerWrapper newTimer) {
final wasRunning = _timer?.isActive ?? false;
nativeTimer = newTimer;
if (nativeTimer.isRunning() && !wasRunning) {
_timer?.cancel();
_timer = Timer(
Duration(
milliseconds: nativeTimer.getRemaining().inMilliseconds % 1000,
), () {
_timer = Timer.periodic(
const Duration(seconds: 1),
(timer) {
nativeTimer.update();
notifyListeners();
},
);
notifyListeners();
});
}
notifyListeners();
}
}
Expand All @@ -29,10 +49,12 @@ void main() {
database = AppDatabase();
android = const MethodChannel("com.presley.flexify/android");
WidgetsFlutterBinding.ensureInitialized();
runApp(ChangeNotifierProvider(
create: (context) => AppState(),
child: const MyApp(),
));
runApp(
ChangeNotifierProvider(
create: (context) => AppState(),
child: const MyApp(),
),
);
}

class MyApp extends StatelessWidget {
Expand Down Expand Up @@ -86,11 +108,11 @@ class _MyHomePageState extends State<MyHomePage>
Widget build(BuildContext context) {
android.setMethodCallHandler((call) async {
if (call.method == 'tick') {
final newTimer = Timer(
Duration(milliseconds: call.arguments[0]),
Duration(milliseconds: call.arguments[1]),
DateTime.fromMillisecondsSinceEpoch(call.arguments[2], isUtc: true),
);
final newTimer = NativeTimerWrapper(
Duration(milliseconds: call.arguments[0]),
Duration(milliseconds: call.arguments[1]),
DateTime.fromMillisecondsSinceEpoch(call.arguments[2], isUtc: true),
NativeTimerState.values[call.arguments[3] as int]);

Provider.of<AppState>(
context,
Expand All @@ -105,8 +127,8 @@ class _MyHomePageState extends State<MyHomePage>
builder: (BuildContext context) {
return Scaffold(
bottomSheet: Consumer<AppState>(builder: (context, value, child) {
final duration = value.timer.getDuration().inSeconds;
final elapsed = value.timer.getElapsed().inSeconds;
final duration = value.nativeTimer.getDuration().inSeconds;
final elapsed = value.nativeTimer.getElapsed().inSeconds;

return Visibility(
visible: duration > 0,
Expand Down

0 comments on commit e8ad86d

Please sign in to comment.