Skip to content

Commit 79ed03f

Browse files
authored
Feat/pause tracking (#77)
This pull request introduces several changes to enhance the activity tracking functionality, including the addition of pause/resume capabilities, updates to the tracking state, and improvements to the user interface. ### Enhancements to Activity Tracking Functionality: * Added a new `ActivityBroadcastReceiver` in `AndroidManifest.xml` to handle pause and stop actions for activity tracking. * Updated the tracking state to include `isPaused` and `durationMillis` fields in `TrackingState` and modified the `copyWith` method accordingly. [[1]](diffhunk://#diff-202d7e63825d16c2b083b05352b9ccfb46fb57a08232c695758033f3a56df224L9-R23) [[2]](diffhunk://#diff-202d7e63825d16c2b083b05352b9ccfb46fb57a08232c695758033f3a56df224L29-R41) * Introduced a `togglePauseTracking` method in `TrackingViewModel` to handle pausing and resuming of activities, replacing the previous `pauseTracking` method. * Modified the `startTimer` and `updateDuration` methods in `TrackingViewModel` to work with `durationMillis` instead of a string duration. ### User Interface Improvements: * Updated `ActiveTracking` widget to display `durationMillis` and handle paused state, including changes to the pause button icon. [[1]](diffhunk://#diff-d49adc2b4b389d095be8fde161b0fcba63a3d310f235e93716a2360f5be45da9L12-R23) [[2]](diffhunk://#diff-d49adc2b4b389d095be8fde161b0fcba63a3d310f235e93716a2360f5be45da9L42-R74) * Enhanced `TrackingRecording` widget to show the elapsed time and current speed, considering the paused state. [[1]](diffhunk://#diff-6321dbec87c91b717c14f686f4ba1bc8f0adb34ea26bce120c9630fd5d3093eeR9-R20) [[2]](diffhunk://#diff-6321dbec87c91b717c14f686f4ba1bc8f0adb34ea26bce120c9630fd5d3093eeL29-R42) [[3]](diffhunk://#diff-6321dbec87c91b717c14f686f4ba1bc8f0adb34ea26bce120c9630fd5d3093eeL48-R52) [[4]](diffhunk://#diff-6321dbec87c91b717c14f686f4ba1bc8f0adb34ea26bce120c9630fd5d3093eeL81-R101) * Adjusted `TrackingFinished` widget to display the total duration in milliseconds. [[1]](diffhunk://#diff-8e932347020831edfb689603f2cd801760565ebfe1a13041289556cf0f781148R14-R26) [[2]](diffhunk://#diff-8e932347020831edfb689603f2cd801760565ebfe1a13041289556cf0f781148L73-R76) [[3]](diffhunk://#diff-8e932347020831edfb689603f2cd801760565ebfe1a13041289556cf0f781148L89-R93) ### Code Refactoring: * Updated various files to replace string duration with `durationMillis` and to include `isPaused` state where necessary. [[1]](diffhunk://#diff-70a7a11b1888ccd7f44bd2466497659e9b19eb98003d549efd994b23d5c7f566L50-R50) [[2]](diffhunk://#diff-70a7a11b1888ccd7f44bd2466497659e9b19eb98003d549efd994b23d5c7f566L172-R177) [[3]](diffhunk://#diff-e9fe2cd457ace53e850809c195f8fa463087c0b63e124a31541a255b5493a965R58-R67) [[4]](diffhunk://#diff-2315f9fe656582eb216ac6a4ebc0d82ccd82b493eabba1af6a7bafa63a5475e3R11-R41) * Simplified event handling in `TrackingViewModel` by using constants from the `Event` class.
2 parents 0709ea4 + 0ae559f commit 79ed03f

12 files changed

Lines changed: 171 additions & 69 deletions

File tree

android/app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@
9191
android:exported="false"
9292
android:foregroundServiceType="location|health" />
9393

94+
<receiver
95+
android:name="de.buseslaar.tracking.activity_tracking.notification.receiver.ActivityBroadcastReceiver"
96+
android:exported="false">
97+
<intent-filter>
98+
<action android:name="de.buseslaar.tracking.PAUSE" />
99+
<action android:name="de.buseslaar.tracking.STOP" />
100+
</intent-filter>
101+
</receiver>
94102
</application>
95103
<!-- Required to query activities that can process text, see:
96104
https://developer.android.com/training/package-visibility and

lib/presentation/activities/details/widget/detail_stats_card.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,20 @@ class DetailStatsCardEntry extends StatelessWidget {
2121
displayName,
2222
style: TextStyle(
2323
fontSize: 14,
24-
color: Theme.of(context).colorScheme.onSurfaceVariant),
24+
color: Theme
25+
.of(context)
26+
.colorScheme
27+
.onSurfaceVariant),
2528
),
2629
]),
2730
Text(value,
2831
textAlign: TextAlign.right,
2932
style: TextStyle(
3033
fontSize: 18,
31-
color: Theme.of(context).colorScheme.onSurface))
34+
color: Theme
35+
.of(context)
36+
.colorScheme
37+
.onSurface))
3238
],
3339
),
3440
if (child != null) child!

lib/presentation/today/screen/today_screen.dart

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class TodayScreen extends HookConsumerWidget {
4747
}
4848

4949
Future<void> pauseTracking() async {
50-
await ref.read(trackingViewModelProvider.notifier).pauseTracking();
50+
await ref.read(trackingViewModelProvider.notifier).togglePauseTracking();
5151
context.push("/tracking");
5252
}
5353

@@ -165,14 +165,16 @@ void _onCardClick(BuildContext context) {
165165
}
166166

167167
Widget _buildCurrentTracking(
168-
BuildContext context,
169-
WidgetRef ref,
170-
TrackingState state,
171-
Future<void> Function() stopTracking,
172-
Future<void> Function() pauseTracking) {
168+
BuildContext context,
169+
WidgetRef ref,
170+
TrackingState state,
171+
Future<void> Function() stopTracking,
172+
Future<void> Function() pauseTracking,
173+
) {
173174
return ActiveTracking(
174175
activity: state.activity,
175-
duration: state.duration,
176+
durationMillis: state.durationMillis,
177+
paused: state.isPaused,
176178
onCardClick: () => _onCardClick(context),
177179
onPause: pauseTracking,
178180
onStop: stopTracking,

lib/presentation/today/widgets/tracking_active.dart

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@ class ActiveTracking extends StatelessWidget {
99
const ActiveTracking(
1010
{super.key,
1111
required this.activity,
12-
required this.duration,
12+
required this.durationMillis,
1313
required this.onStop,
1414
required this.onPause,
15+
required this.paused,
1516
required this.onCardClick});
1617

1718
final Activity activity;
18-
final String duration;
19+
final int durationMillis;
1920
final Future<void> Function() onStop;
2021
final Future<void> Function() onPause;
2122
final Function onCardClick;
23+
final bool paused;
2224

2325
@override
2426
Widget build(BuildContext context) {
@@ -39,29 +41,37 @@ class ActiveTracking extends StatelessWidget {
3941
getIcon(activity.activityType ?? ActivityType.unknown,
4042
color: Theme.of(context).colorScheme.onPrimary),
4143
const SizedBox(width: 8),
42-
Text(
43-
getTranslatedActivityType(
44-
context,
45-
HealthWorkoutActivityType.values.firstWhere(
46-
(element) =>
47-
element.name ==
48-
activity.activityType?.name)),
49-
style: const TextStyle(
50-
color: Colors.white, fontSize: 20),
51-
),
44+
Container(
45+
width: 120,
46+
child: Text(
47+
getTranslatedActivityType(
48+
context,
49+
HealthWorkoutActivityType.values.firstWhere(
50+
(element) =>
51+
element.name ==
52+
activity.activityType?.name)),
53+
style: const TextStyle(
54+
color: Colors.white,
55+
fontSize: 20,
56+
overflow: TextOverflow.ellipsis,
57+
),
58+
)),
5259
],
5360
),
5461
const SizedBox(width: 8),
5562
// Center with distance
5663
Text(
57-
duration,
64+
getDuration(durationMillis),
5865
style: const TextStyle(color: Colors.white, fontSize: 20),
5966
),
6067

6168
// Right side with buttons
6269
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
63-
_buildActionButton(context, onPause,
64-
Theme.of(context).colorScheme.outline, Icons.pause),
70+
_buildActionButton(
71+
context,
72+
onPause,
73+
Theme.of(context).colorScheme.outline,
74+
paused ? Icons.play_arrow : Icons.pause),
6575
_buildActionButton(context, onStop,
6676
Theme.of(context).colorScheme.error, Icons.stop)
6777
]),

lib/presentation/tracking/screen/tracking.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,16 @@ class TrackingScreen extends HookConsumerWidget {
5555
.read(trackingViewModelProvider.notifier)
5656
.stopTracking();
5757
},
58+
onPause: () {
59+
ref
60+
.read(trackingViewModelProvider.notifier)
61+
.togglePauseTracking();
62+
},
5863
startTimer:
5964
ref.read(trackingViewModelProvider.notifier).startTimer,
60-
duration: trackingState.duration,
61-
isRecording: trackingState!.isRecording,
65+
durationMillis: trackingState.durationMillis,
66+
isRecording: trackingState.isRecording,
67+
isPaused: trackingState.isPaused,
6268
)));
6369
}
6470
}

lib/presentation/tracking/view_model/tracking_state.dart

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ import 'package:logging/logging.dart';
66
class TrackingState {
77
Activity activity;
88
bool isRecording = false;
9-
String duration = "--";
9+
bool isPaused = false;
10+
int durationMillis = 0;
1011
final log = Logger('trackingState');
1112

1213
TrackingState(
1314
{required this.activity,
1415
required this.isRecording,
15-
required this.duration});
16+
required this.isPaused,
17+
required this.durationMillis});
1618

1719
factory TrackingState.initial() {
1820
return TrackingState(
1921
isRecording: false,
20-
duration: "--",
22+
isPaused: false,
23+
durationMillis: 0,
2124
activity: Activity(
2225
activityType: ActivityType.unknown,
2326
distance: 0.0,
@@ -26,10 +29,15 @@ class TrackingState {
2629
}
2730

2831
TrackingState copyWith(
29-
{Activity? newActivity, bool? newIsRecording, String? newDuration}) {
32+
{Activity? newActivity,
33+
bool? newIsRecording,
34+
bool? newIsPaused,
35+
int? newDurationMillis}) {
3036
return TrackingState(
31-
isRecording: newIsRecording ?? isRecording,
32-
duration: newDuration ?? duration,
33-
activity: newActivity ?? activity);
37+
isRecording: newIsRecording ?? isRecording,
38+
activity: newActivity ?? activity,
39+
isPaused: newIsPaused ?? isPaused,
40+
durationMillis: newDurationMillis ?? durationMillis,
41+
);
3442
}
3543
}

lib/presentation/tracking/view_model/tracking_view_model.dart

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'dart:convert';
44
import 'package:activity_tracking/activity_tracking.dart';
55
import 'package:activity_tracking/model/activity.dart';
66
import 'package:activity_tracking/model/activity_type.dart';
7+
import 'package:activity_tracking/model/event.dart';
78
import 'package:activity_tracking/model/message.dart';
89
import 'package:logging/logging.dart';
910
import 'package:movetopia/data/repositories/local_health_impl.dart';
@@ -33,19 +34,33 @@ class TrackingViewModel extends StateNotifier<TrackingState?> {
3334
await activityTrackingPlugin.startActivity(activityType);
3435
state = state?.copyWith(newActivity: startedActivity, newIsRecording: true);
3536
activityStreamSubscription =
36-
activityTrackingPlugin.getNativeEvents().listen(_onActivityUpdate);
37+
activityTrackingPlugin.getNativeEvents().listen((e) {
38+
_onActivityUpdate(e);
39+
});
3740
}
3841

39-
pauseTracking() async {
40-
print("pauseTracking");
42+
togglePauseTracking() async {
43+
if (state!.isRecording) {
44+
// result indicates whether the toggle was successful or not
45+
var result = await activityTrackingPlugin.togglePauseActivity();
46+
if (result != null && result) {
47+
state = state?.copyWith(newIsPaused: !state!.isPaused);
48+
49+
log.info("Activity paused: ${state?.isPaused}");
50+
if (state?.isPaused == false) {
51+
startTimer();
52+
}
53+
} else {
54+
log.warning("Failed to pause activity");
55+
}
56+
}
4157
}
4258

4359
stopTracking() async {
4460
await activityStreamSubscription.cancel();
4561
var finalResult = await activityTrackingPlugin.stopCurrentActivity();
4662
if (finalResult != null) {
47-
state = state?.copyWith(
48-
newActivity: finalResult, newIsRecording: false, newDuration: "");
63+
state = state?.copyWith(newActivity: finalResult, newIsRecording: false);
4964

5065
await ref
5166
.read(localHealthRepositoryProvider)
@@ -55,19 +70,19 @@ class TrackingViewModel extends StateNotifier<TrackingState?> {
5570

5671
void startTimer() {
5772
Future.delayed(const Duration(seconds: 1), () {
58-
ref.read(trackingViewModelProvider.notifier).updateDuration(getDuration(
59-
state?.activity.startDateTime ?? 0,
60-
DateTime.now().millisecondsSinceEpoch));
61-
if (state?.isRecording == true) {
73+
ref
74+
.read(trackingViewModelProvider.notifier)
75+
.updateDuration(state!.durationMillis + 1000);
76+
if (state?.isRecording == true && state?.isPaused == false) {
6277
startTimer();
6378
} else {
6479
return;
6580
}
6681
});
6782
}
6883

69-
updateDuration(String duration) {
70-
state = state?.copyWith(newDuration: duration);
84+
updateDuration(int durationMillis) {
85+
state = state?.copyWith(newDurationMillis: durationMillis);
7186
}
7287

7388
clearState() {
@@ -77,24 +92,43 @@ class TrackingViewModel extends StateNotifier<TrackingState?> {
7792
_onActivityUpdate(dynamic e) {
7893
var eventMessage = Message.fromJson(jsonDecode(e));
7994
switch (eventMessage.type) {
80-
case "step":
95+
case Event.step:
8196
state?.activity.steps =
8297
((state?.activity.steps ?? 0) + (eventMessage.data ?? 0)) as int?;
8398
state =
8499
state?.copyWith(newActivity: state?.activity, newIsRecording: true);
85-
case "location":
100+
case Event.location:
86101
if (eventMessage.data != null) {
87102
state?.activity.locations?.addAll(eventMessage.data);
88103
state = state?.copyWith(
89104
newActivity: state?.activity, newIsRecording: true);
90105
}
91106

92-
case "distance":
107+
case Event.distance:
93108
if (eventMessage.data != null && eventMessage.data != 0) {
94109
state?.activity.distance = eventMessage.data;
95110
state = state?.copyWith(
96111
newActivity: state?.activity, newIsRecording: true);
97112
}
113+
case Event.pause:
114+
if (eventMessage.data != null) {
115+
state = state?.copyWith(
116+
newActivity: eventMessage.data as Activity, newIsPaused: false);
117+
}
118+
case Event.resume:
119+
if (eventMessage.data != null) {
120+
state = state?.copyWith(
121+
newActivity: eventMessage.data as Activity, newIsPaused: true);
122+
startTimer();
123+
}
124+
case Event.stop:
125+
if (eventMessage.data != null) {
126+
state = state?.copyWith(
127+
newActivity: eventMessage.data as Activity,
128+
newIsRecording: false);
129+
}
130+
case null:
131+
log.info("Event is null");
98132
}
99133
}
100134
}

lib/presentation/tracking/widgets/current_activity.dart

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,36 @@ class CurrentActivity extends StatelessWidget {
88

99
final Function onStop;
1010

11+
final Function onPause;
12+
1113
final bool isRecording;
1214

13-
final String duration;
15+
final bool isPaused;
16+
17+
final int durationMillis;
1418

1519
final Function startTimer;
1620

1721
const CurrentActivity(
1822
{super.key,
1923
required this.activity,
2024
required this.onStop,
25+
required this.onPause,
2126
required this.isRecording,
2227
required this.startTimer,
23-
required this.duration});
28+
required this.durationMillis,
29+
required this.isPaused});
2430

2531
@override
2632
Widget build(BuildContext context) {
2733
return isRecording
2834
? TrackingRecording(
2935
activity: activity,
3036
onStop: onStop,
31-
duration: duration,
37+
durationMillis: durationMillis,
38+
isPaused: isPaused,
39+
onPause: onPause,
3240
)
33-
: TrackingFinished(activity: activity);
41+
: TrackingFinished(activity: activity, durationMillis: durationMillis);
3442
}
3543
}

0 commit comments

Comments
 (0)