fix(watch): corrige applicationId et nettoie la config de build montre
Répare la régression de packaging introduite par le reset/remerge de develop : l'APK montre était packagée avec com.gametime.app au lieu de com.gametime.watch (namespace correct mais applicationId hérité du téléphone), ce qui cassait la détection du bridge. Aligne applicationId sur com.gametime.watch, nettoie la config de signing partagée sur les deux modules android/app et watch_app/android/app, et ajuste use_cases + watch_session_screen en conséquence. Validé : APK montre release package = com.gametime.watch, même SHA-256 de certificat sur les deux APKs (70cc69e8...45c7815), tests use_cases et watch_session_screen OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,3 +1,5 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
@ -5,6 +7,26 @@ plugins {
|
|||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sharedSigningPropertiesFile = rootProject.file("key.properties")
|
||||||
|
val sharedSigningProperties =
|
||||||
|
Properties().also { properties ->
|
||||||
|
check(sharedSigningPropertiesFile.isFile) {
|
||||||
|
"Shared signing config missing: ${sharedSigningPropertiesFile.absolutePath}"
|
||||||
|
}
|
||||||
|
sharedSigningPropertiesFile.inputStream().use(properties::load)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requiredSharedSigningProperty(name: String): String =
|
||||||
|
sharedSigningProperties.getProperty(name)?.takeIf { it.isNotBlank() }
|
||||||
|
?: error("Shared signing property '$name' missing in ${sharedSigningPropertiesFile.absolutePath}")
|
||||||
|
|
||||||
|
val sharedSigningStoreFile =
|
||||||
|
sharedSigningPropertiesFile.parentFile.resolve(requiredSharedSigningProperty("storeFile"))
|
||||||
|
|
||||||
|
check(sharedSigningStoreFile.isFile) {
|
||||||
|
"Shared signing keystore missing: ${sharedSigningStoreFile.absolutePath}"
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.gametime.app"
|
namespace = "com.gametime.app"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
@ -23,11 +45,18 @@ android {
|
|||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("sharedLocal") {
|
||||||
|
storeFile = sharedSigningStoreFile
|
||||||
|
storePassword = requiredSharedSigningProperty("storePassword")
|
||||||
|
keyAlias = requiredSharedSigningProperty("keyAlias")
|
||||||
|
keyPassword = requiredSharedSigningProperty("keyPassword")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
signingConfig = signingConfigs.getByName("sharedLocal")
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5647,41 +5647,68 @@ double _estimatedCaloriesKcal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class WorkoutHistoryUseCases {
|
final class WorkoutHistoryUseCases {
|
||||||
const WorkoutHistoryUseCases({required this.repository, required this.clock});
|
WorkoutHistoryUseCases({required this.repository, required this.clock});
|
||||||
|
|
||||||
final WorkoutHistoryRepository repository;
|
final WorkoutHistoryRepository repository;
|
||||||
final Clock clock;
|
final Clock clock;
|
||||||
|
final _pendingHeartRateSummaries = <String, WatchSensorSummary>{};
|
||||||
|
|
||||||
Future<WorkoutHistory?> findById(String id) => repository.findById(id);
|
Future<WorkoutHistory?> findById(String id) async {
|
||||||
|
final history = await repository.findById(id);
|
||||||
|
if (history == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final changed = await _applyPendingHeartRateSummaries([history]);
|
||||||
|
return changed ? repository.findById(id) : history;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<WorkoutHistory>> listActive() => repository.listActive();
|
Future<List<WorkoutHistory>> listActive() async {
|
||||||
|
final histories = await repository.listActive();
|
||||||
|
final changed = await _applyPendingHeartRateSummaries(histories);
|
||||||
|
return changed ? repository.listActive() : histories;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> delete(String id) => repository.delete(id, clock.now());
|
Future<void> delete(String id) => repository.delete(id, clock.now());
|
||||||
|
|
||||||
Future<void> updateHeartRateSummary(WatchSensorSummary summary) async {
|
Future<void> updateHeartRateSummary(WatchSensorSummary summary) async {
|
||||||
if (summary.sampleCount < 3 ||
|
if (!_isUsableHeartRateSummary(summary)) {
|
||||||
summary.averageHeartRateBpm == null ||
|
|
||||||
summary.maxHeartRateBpm == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (summary.sessionId.trim().isEmpty ||
|
|
||||||
summary.averageHeartRateBpm! <= 0 ||
|
|
||||||
summary.maxHeartRateBpm! <= 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final sessionId = summary.sessionId.trim();
|
||||||
final histories = await repository.listActive();
|
final histories = await repository.listActive();
|
||||||
WorkoutHistory? history;
|
final applied = await _applyHeartRateSummary(histories, summary);
|
||||||
for (final candidate in histories) {
|
if (applied) {
|
||||||
if (candidate.metadata.id == summary.sessionId ||
|
_pendingHeartRateSummaries.remove(sessionId);
|
||||||
candidate.sourceActiveWorkoutSessionId == summary.sessionId) {
|
return;
|
||||||
history = candidate;
|
}
|
||||||
break;
|
if (!_hasHistoryForSummary(histories, summary)) {
|
||||||
|
_pendingHeartRateSummaries.putIfAbsent(sessionId, () => summary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> _applyPendingHeartRateSummaries(
|
||||||
|
List<WorkoutHistory> histories,
|
||||||
|
) async {
|
||||||
|
var changed = false;
|
||||||
|
for (final summary in _pendingHeartRateSummaries.values.toList()) {
|
||||||
|
final applied = await _applyHeartRateSummary(histories, summary);
|
||||||
|
if (applied) {
|
||||||
|
_pendingHeartRateSummaries.remove(summary.sessionId.trim());
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _applyHeartRateSummary(
|
||||||
|
List<WorkoutHistory> histories,
|
||||||
|
WatchSensorSummary summary,
|
||||||
|
) async {
|
||||||
|
final history = _findHistoryForSummary(histories, summary);
|
||||||
if (history == null ||
|
if (history == null ||
|
||||||
history.averageHeartRateBpm != null ||
|
history.averageHeartRateBpm != null ||
|
||||||
history.maxHeartRateBpm != null) {
|
history.maxHeartRateBpm != null) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
await repository.patchHeartRateSummary(
|
await repository.patchHeartRateSummary(
|
||||||
historyId: history.metadata.id,
|
historyId: history.metadata.id,
|
||||||
@ -5689,6 +5716,37 @@ final class WorkoutHistoryUseCases {
|
|||||||
maxHeartRateBpm: summary.maxHeartRateBpm!,
|
maxHeartRateBpm: summary.maxHeartRateBpm!,
|
||||||
patchedAt: clock.now(),
|
patchedAt: clock.now(),
|
||||||
);
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasHistoryForSummary(
|
||||||
|
List<WorkoutHistory> histories,
|
||||||
|
WatchSensorSummary summary,
|
||||||
|
) {
|
||||||
|
return _findHistoryForSummary(histories, summary) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkoutHistory? _findHistoryForSummary(
|
||||||
|
List<WorkoutHistory> histories,
|
||||||
|
WatchSensorSummary summary,
|
||||||
|
) {
|
||||||
|
final sessionId = summary.sessionId.trim();
|
||||||
|
for (final candidate in histories) {
|
||||||
|
if (candidate.metadata.id == sessionId ||
|
||||||
|
candidate.sourceActiveWorkoutSessionId == sessionId) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isUsableHeartRateSummary(WatchSensorSummary summary) {
|
||||||
|
return summary.sampleCount >= 3 &&
|
||||||
|
summary.sessionId.trim().isNotEmpty &&
|
||||||
|
summary.averageHeartRateBpm != null &&
|
||||||
|
summary.averageHeartRateBpm! > 0 &&
|
||||||
|
summary.maxHeartRateBpm != null &&
|
||||||
|
summary.maxHeartRateBpm! > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2864,6 +2864,53 @@ void main() {
|
|||||||
expect(repository.histories.single.maxHeartRateBpm, 171);
|
expect(repository.histories.single.maxHeartRateBpm, 171);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'WorkoutHistoryUseCases applies pending heart rate summary when history appears',
|
||||||
|
() async {
|
||||||
|
final repository = _FakeWorkoutHistoryRepository();
|
||||||
|
final useCase = WorkoutHistoryUseCases(
|
||||||
|
repository: repository,
|
||||||
|
clock: _FakeClock(DateTime.utc(2026, 7, 25, 12, 1)),
|
||||||
|
);
|
||||||
|
|
||||||
|
await useCase.updateHeartRateSummary(
|
||||||
|
const WatchSensorSummary(
|
||||||
|
sessionId: 'session-1',
|
||||||
|
sampleCount: 12,
|
||||||
|
averageHeartRateBpm: 126.5,
|
||||||
|
maxHeartRateBpm: 171,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.histories.add(
|
||||||
|
WorkoutHistory(
|
||||||
|
metadata: _metadata('history-1'),
|
||||||
|
sourceActiveWorkoutSessionId: 'session-1',
|
||||||
|
nameSnapshot: 'Seance',
|
||||||
|
startedAt: DateTime.utc(2026, 7, 25, 11),
|
||||||
|
endedAt: DateTime.utc(2026, 7, 25, 12),
|
||||||
|
totalActiveMs: 3600000,
|
||||||
|
completed: true,
|
||||||
|
historySnapshotJson: '{"programs":[]}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final histories = await useCase.listActive();
|
||||||
|
|
||||||
|
expect(histories.single.averageHeartRateBpm, 126.5);
|
||||||
|
expect(histories.single.maxHeartRateBpm, 171);
|
||||||
|
await useCase.updateHeartRateSummary(
|
||||||
|
const WatchSensorSummary(
|
||||||
|
sessionId: 'session-1',
|
||||||
|
sampleCount: 14,
|
||||||
|
averageHeartRateBpm: 130,
|
||||||
|
maxHeartRateBpm: 180,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(repository.histories.single.averageHeartRateBpm, 126.5);
|
||||||
|
expect(repository.histories.single.maxHeartRateBpm, 171);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'ActiveWorkoutSensorUseCases tracks live heart rate and calories',
|
'ActiveWorkoutSensorUseCases tracks live heart rate and calories',
|
||||||
() async {
|
() async {
|
||||||
|
|||||||
@ -1,9 +1,32 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sharedSigningPropertiesFile =
|
||||||
|
rootProject.projectDir.parentFile.parentFile.resolve("android/key.properties")
|
||||||
|
val sharedSigningProperties =
|
||||||
|
Properties().also { properties ->
|
||||||
|
check(sharedSigningPropertiesFile.isFile) {
|
||||||
|
"Shared signing config missing: ${sharedSigningPropertiesFile.absolutePath}"
|
||||||
|
}
|
||||||
|
sharedSigningPropertiesFile.inputStream().use(properties::load)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requiredSharedSigningProperty(name: String): String =
|
||||||
|
sharedSigningProperties.getProperty(name)?.takeIf { it.isNotBlank() }
|
||||||
|
?: error("Shared signing property '$name' missing in ${sharedSigningPropertiesFile.absolutePath}")
|
||||||
|
|
||||||
|
val sharedSigningStoreFile =
|
||||||
|
sharedSigningPropertiesFile.parentFile.resolve(requiredSharedSigningProperty("storeFile"))
|
||||||
|
|
||||||
|
check(sharedSigningStoreFile.isFile) {
|
||||||
|
"Shared signing keystore missing: ${sharedSigningStoreFile.absolutePath}"
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.gametime.watch"
|
namespace = "com.gametime.watch"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
@ -15,16 +38,25 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.gametime.app"
|
applicationId = "com.gametime.watch"
|
||||||
minSdk = 30
|
minSdk = 30
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("sharedLocal") {
|
||||||
|
storeFile = sharedSigningStoreFile
|
||||||
|
storePassword = requiredSharedSigningProperty("storePassword")
|
||||||
|
keyAlias = requiredSharedSigningProperty("keyAlias")
|
||||||
|
keyPassword = requiredSharedSigningProperty("keyPassword")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("sharedLocal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -679,10 +679,6 @@ final class _ActiveContent extends StatelessWidget {
|
|||||||
_SmallLabel(dominantLabel),
|
_SmallLabel(dominantLabel),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
_DominantValue(dominantValue),
|
_DominantValue(dominantValue),
|
||||||
if (_heartRateLabel(state.sensorSample) case final heartRate?) ...[
|
|
||||||
const SizedBox(height: 1),
|
|
||||||
_LiveHeartRateLine(heartRate),
|
|
||||||
],
|
|
||||||
if (timer != null) ...[
|
if (timer != null) ...[
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
_TimerToggleButton(
|
_TimerToggleButton(
|
||||||
@ -796,10 +792,6 @@ final class _ManualScoreContent extends StatelessWidget {
|
|||||||
? const _PendingDot()
|
? const _PendingDot()
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
if (_heartRateLabel(state.sensorSample) case final heartRate?) ...[
|
|
||||||
const SizedBox(height: 1),
|
|
||||||
_LiveHeartRateLine(heartRate),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
if (timer != null)
|
if (timer != null)
|
||||||
_CompactTimerLine(
|
_CompactTimerLine(
|
||||||
@ -942,33 +934,6 @@ final class _PendingDot extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _LiveHeartRateLine extends StatelessWidget {
|
|
||||||
const _LiveHeartRateLine(this.label);
|
|
||||||
|
|
||||||
final String label;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.favorite, size: 11, color: Color(0xFFD72638)),
|
|
||||||
const SizedBox(width: 3),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
||||||
color: const Color(0xFFA7ADBA),
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class _RestContent extends StatelessWidget {
|
final class _RestContent extends StatelessWidget {
|
||||||
const _RestContent({required this.state, required this.onTogglePause});
|
const _RestContent({required this.state, required this.onTogglePause});
|
||||||
|
|
||||||
|
|||||||
@ -212,7 +212,7 @@ void main() {
|
|||||||
viewModel.dispose();
|
viewModel.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('shows live heart rate on session and telemetry on stats page', (
|
testWidgets('shows telemetry only on stats page when available', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
final client = _FakeNativeWatchBridgeClient();
|
final client = _FakeNativeWatchBridgeClient();
|
||||||
@ -245,7 +245,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(find.text('142 bpm'), findsOneWidget);
|
expect(find.text('142 bpm'), findsNothing);
|
||||||
expect(find.byTooltip('Stats'), findsOneWidget);
|
expect(find.byTooltip('Stats'), findsOneWidget);
|
||||||
|
|
||||||
await tester.drag(
|
await tester.drag(
|
||||||
@ -261,6 +261,7 @@ void main() {
|
|||||||
|
|
||||||
expect(find.text('Stats'), findsOneWidget);
|
expect(find.text('Stats'), findsOneWidget);
|
||||||
expect(find.text('FC'), findsOneWidget);
|
expect(find.text('FC'), findsOneWidget);
|
||||||
|
expect(find.text('142 bpm'), findsOneWidget);
|
||||||
expect(find.text('840 m'), findsOneWidget);
|
expect(find.text('840 m'), findsOneWidget);
|
||||||
expect(find.text('186 kcal'), findsOneWidget);
|
expect(find.text('186 kcal'), findsOneWidget);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user