merge(develop): corrige applicationId et signature partagée de l'APK montre
Intègre le correctif de packaging/signature de watch_app (applicationId com.gametime.watch, signing partagé nettoyé) qui répare la détection du bridge côté montre après le reset/remerge de develop. QA verte. 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