Skip to content

Commit

Permalink
Fixed UI bug causing discrepency
Browse files Browse the repository at this point in the history
Switched back to TimerHandler from kotlin.concurrent.fixedRateTimer
Reduced number of broadcasts
Updated refresh rate to 20ms for both UI and Notification
  • Loading branch information
josephnglynn committed Mar 11, 2024
1 parent 731ff63 commit b294b55
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 54 deletions.
13 changes: 11 additions & 2 deletions android/app/src/main/kotlin/com/presley/flexify/FlexifyTimer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class FlexifyTimer(private var msTimerDuration: Long) {

fun start(context: Context, elapsedTime: Long = 0) {
if (state != State.Paused) return
endTime = SystemClock.elapsedRealtime() + msTimerDuration - elapsedTime
msTimerDuration -= elapsedTime
endTime = SystemClock.elapsedRealtime() + msTimerDuration
registerPendingIntent(context)
state = State.Running
}
Expand Down Expand Up @@ -79,11 +80,18 @@ class FlexifyTimer(private var msTimerDuration: Long) {
return longArrayOf(
totalTimerDuration,
totalTimerDuration - getRemainingMillis(),
java.lang.System.currentTimeMillis(),
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)
Expand Down Expand Up @@ -167,6 +175,7 @@ class FlexifyTimer(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

Expand Down
73 changes: 44 additions & 29 deletions android/app/src/main/kotlin/com/presley/flexify/TimerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,15 @@ import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
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 = ""
private var updateLoopTimer: Timer = Timer();
var flexifyTimer: FlexifyTimer = FlexifyTimer.emptyTimer()


Expand All @@ -49,10 +46,11 @@ class TimerService : Service() {
flexifyTimer.stop(applicationContext)
flexifyTimer.expire()

updateLoopTimer.cancel()
timerRunnable?.let { timerHandler.removeCallbacks(it) }
mediaPlayer?.stop()
vibrator?.cancel()
updateAppUI()

if (intent != null && intent.action == STOP_BROADCAST_INTERNAL) updateAppUI()
val notificationManager = NotificationManagerCompat.from(this@TimerService)
notificationManager.cancel(ONGOING_ID)
stopForeground(STOP_FOREGROUND_REMOVE)
Expand All @@ -64,35 +62,48 @@ class TimerService : Service() {
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d("TimerService", "Received add broadcast intent")
val timeStamp = intent?.getLongExtra("timeStamp", 0)
if (flexifyTimer.isExpired()) return startTimer(
FlexifyTimer.ONE_MINUTE_MILLI,
intent?.getLongExtra("timeStamp", 0) ?: 0
timeStamp ?: 0
)

flexifyTimer.increaseDuration(applicationContext, FlexifyTimer.ONE_MINUTE_MILLI)
updateNotification(flexifyTimer.getRemainingSeconds())
mediaPlayer?.stop()
vibrator?.cancel()
updateAppUI()
if (intent != null && intent.action == ADD_BROADCAST_INTERNAL) 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),
stopReceiver, IntentFilter().apply {
addAction(STOP_BROADCAST)
addAction(STOP_BROADCAST_INTERNAL)
},
Context.RECEIVER_NOT_EXPORTED
)
applicationContext.registerReceiver(
addReceiver, IntentFilter(ADD_BROADCAST),
addReceiver, IntentFilter().apply {
addAction(ADD_BROADCAST)
addAction(ADD_BROADCAST_INTERNAL)
},
Context.RECEIVER_NOT_EXPORTED
)
} else {
applicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST))
applicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST))
applicationContext.registerReceiver(stopReceiver, IntentFilter().apply {
addAction(STOP_BROADCAST)
addAction(STOP_BROADCAST_INTERNAL)
})
applicationContext.registerReceiver(addReceiver, IntentFilter().apply {
addAction(ADD_BROADCAST)
addAction(ADD_BROADCAST_INTERNAL)
})
}
}

Expand All @@ -110,13 +121,13 @@ class TimerService : Service() {
}

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

flexifyTimer.stop(applicationContext)
flexifyTimer = FlexifyTimer(msDuration)
flexifyTimer.start(
applicationContext,
if (timeStamp > 0) System.currentTimeMillis() - timeStamp else 0
if (timeStamp > 0) System.currentTimeMillis() - timeStamp else 0,
)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
Expand All @@ -132,16 +143,16 @@ class TimerService : Service() {
battery()
Log.d("TimerService", "onTimerStart seconds=${flexifyTimer.getDurationSeconds()}")

updateLoopTimer = fixedRateTimer(
"updateNotificationUI",
false,
flexifyTimer.getRemainingMillis() % 1000,
1000
) {
if (!flexifyTimer.isExpired()) updateNotification(flexifyTimer.getRemainingSeconds())
timerRunnable = object : Runnable {
override fun run() {
if (flexifyTimer.isExpired()) return
if (flexifyTimer.hasSecondsUpdated()) updateNotification(flexifyTimer.getRemainingSeconds())
timerHandler.postDelayed(this, flexifyTimer.getRemainingMillis() % 20)
}
}
timerHandler.postDelayed(timerRunnable!!, 20)

updateAppUI()
if (timeStamp == 0.toLong()) updateAppUI()
}

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

override fun onDestroy() {
super.onDestroy()
updateLoopTimer.cancel()
timerHandler.removeCallbacks(timerRunnable!!)
applicationContext.unregisterReceiver(stopReceiver)
applicationContext.unregisterReceiver(addReceiver)
mediaPlayer?.stop()
Expand Down Expand Up @@ -208,7 +219,7 @@ class TimerService : Service() {
contentIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val stopBroadcast = Intent(STOP_BROADCAST)
val stopBroadcast = Intent(STOP_BROADCAST_INTERNAL)
stopBroadcast.setPackage(applicationContext.packageName)
val stopPending =
PendingIntent.getBroadcast(
Expand All @@ -218,7 +229,9 @@ class TimerService : Service() {
PendingIntent.FLAG_IMMUTABLE
)
val addBroadcast =
Intent(ADD_BROADCAST).apply { setPackage(applicationContext.packageName) }
Intent(ADD_BROADCAST_INTERNAL).apply {
setPackage(applicationContext.packageName)
}
val addPending =
PendingIntent.getBroadcast(
applicationContext,
Expand Down Expand Up @@ -299,7 +312,7 @@ class TimerService : Service() {
contentIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val stopBroadcast = Intent(STOP_BROADCAST)
val stopBroadcast = Intent(STOP_BROADCAST_INTERNAL)
stopBroadcast.setPackage(applicationContext.packageName)
val pendingStop =
PendingIntent.getBroadcast(
Expand All @@ -309,7 +322,7 @@ class TimerService : Service() {
PendingIntent.FLAG_IMMUTABLE
)
val addBroadcast =
Intent(ADD_BROADCAST).apply { setPackage(applicationContext.packageName) }
Intent(ADD_BROADCAST_INTERNAL).apply { setPackage(applicationContext.packageName) }
val addPending =
PendingIntent.getBroadcast(
applicationContext,
Expand Down Expand Up @@ -361,7 +374,9 @@ class TimerService : Service() {

companion object {
const val STOP_BROADCAST = "stop-timer-event"
const val STOP_BROADCAST_INTERNAL = "stop-timer-event-internal"
const val ADD_BROADCAST = "add-timer-event"
const val ADD_BROADCAST_INTERNAL = "add-timer-event-internal"
const val TIMER_EXPIRED = "timer-expired-event"
const val ONGOING_ID = 1
const val FINISHED_ID = 1
Expand Down
26 changes: 14 additions & 12 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'dart:async';

import 'package:flexify/native_timer_wrapper.dart';
import 'package:flexify/database.dart';
import 'package:flexify/graphs_page.dart';
import 'package:flexify/native_timer_wrapper.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
Expand All @@ -29,7 +29,7 @@ class AppState extends ChangeNotifier {
updateTimer(newTimer);
android.invokeMethod('add', [newTimer.getTimeStamp()]);
}

void stopTimer() {
updateTimer(NativeTimerWrapper.emptyTimer());
android.invokeMethod('stop');
Expand All @@ -38,7 +38,8 @@ class AppState extends ChangeNotifier {
void startTimer(String exercise, Duration duration) {
final timer = nativeTimer.increaseDuration(duration);
updateTimer(timer);
android.invokeMethod('timer', [duration.inMilliseconds, exercise, timer.getTimeStamp()]);
android.invokeMethod(
'timer', [duration.inMilliseconds, exercise, timer.getTimeStamp()]);
}

void updateTimer(NativeTimerWrapper newTimer) {
Expand All @@ -47,7 +48,7 @@ class AppState extends ChangeNotifier {
if (nativeTimer.isRunning() && !wasRunning) {
_timer?.cancel();
_timer = Timer.periodic(
const Duration(seconds: 1),
const Duration(milliseconds: 20),
(timer) {
if (nativeTimer.update()) _timer?.cancel();
notifyListeners();
Expand Down Expand Up @@ -122,10 +123,11 @@ class _MyHomePageState extends State<MyHomePage>
android.setMethodCallHandler((call) async {
if (call.method == 'tick') {
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]);
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 @@ -140,13 +142,13 @@ class _MyHomePageState extends State<MyHomePage>
builder: (BuildContext context) {
return Scaffold(
bottomSheet: Consumer<AppState>(builder: (context, value, child) {
final duration = value.nativeTimer.getDuration().inSeconds;
final elapsed = value.nativeTimer.getElapsed().inSeconds;
final duration = value.nativeTimer.getDuration();
final elapsed = value.nativeTimer.getElapsed();

return Visibility(
visible: duration > 0,
visible: duration > Duration.zero,
child: LinearProgressIndicator(
value: duration == 0 ? 0 : elapsed / duration,
value: duration == Duration.zero ? 0 : elapsed.inMilliseconds / duration.inMilliseconds,
),
);
}),
Expand Down
23 changes: 12 additions & 11 deletions lib/timer_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,29 @@ class TimerPage extends StatefulWidget {
}

class _TimerPageState extends State<TimerPage> {
String generateTitleText(int duration, int elapsed) {
final minutes = ((duration - elapsed) ~/ 60).toString().padLeft(2, '0');
final seconds = ((duration - elapsed) % 60).toString().padLeft(2, '0');
String generateTitleText(Duration remaining) {
final minutes = (remaining.inMinutes).toString().padLeft(2, '0');
final seconds = (remaining.inSeconds % 60).toString().padLeft(2, '0');
return "$minutes:$seconds";
}

@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
final duration = appState.nativeTimer.getDuration().inSeconds;
final elapsed = appState.nativeTimer.getElapsed().inSeconds;
final duration = appState.nativeTimer.getDuration();
final elapsed = appState.nativeTimer.getElapsed();
final remaining = appState.nativeTimer.getRemaining();

return Scaffold(
appBar: AppBar(
title: const Text('Timer'),
title: Text('Timer'),
),
body: Center(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
progressWidget(context, duration, elapsed),
textWidget(context, duration, elapsed),
textWidget(context, remaining),
],
),
),
Expand All @@ -46,13 +47,13 @@ class _TimerPageState extends State<TimerPage> {
);
}

SizedBox progressWidget(BuildContext context, int duration, int elapsed) {
SizedBox progressWidget(BuildContext context, Duration duration, Duration elapsed) {
return SizedBox(
height: 300,
width: 300,
child: CircularProgressIndicator(
strokeCap: StrokeCap.round,
value: duration == 0 ? 0 : elapsed / duration,
value: duration == Duration.zero ? 0 : elapsed.inMilliseconds / duration.inMilliseconds,
strokeWidth: 20,
backgroundColor:
Theme.of(context).colorScheme.onSurface.withOpacity(0.25),
Expand All @@ -62,13 +63,13 @@ class _TimerPageState extends State<TimerPage> {
);
}

Column textWidget(BuildContext context, int duration, int elapsed) {
Column textWidget(BuildContext context, Duration remaining) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 32.0),
Text(
generateTitleText(duration, elapsed),
generateTitleText(remaining),
style: TextStyle(
fontSize: 50.0,
color: Theme.of(context).textTheme.bodyLarge!.color,
Expand Down

0 comments on commit b294b55

Please sign in to comment.