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 {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
@ -5,6 +7,26 @@ plugins {
|
||||
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 {
|
||||
namespace = "com.gametime.app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@ -23,11 +45,18 @@ android {
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("sharedLocal") {
|
||||
storeFile = sharedSigningStoreFile
|
||||
storePassword = requiredSharedSigningProperty("storePassword")
|
||||
keyAlias = requiredSharedSigningProperty("keyAlias")
|
||||
keyPassword = requiredSharedSigningProperty("keyPassword")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("sharedLocal")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5647,41 +5647,68 @@ double _estimatedCaloriesKcal({
|
||||
}
|
||||
|
||||
final class WorkoutHistoryUseCases {
|
||||
const WorkoutHistoryUseCases({required this.repository, required this.clock});
|
||||
WorkoutHistoryUseCases({required this.repository, required this.clock});
|
||||
|
||||
final WorkoutHistoryRepository repository;
|
||||
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> updateHeartRateSummary(WatchSensorSummary summary) async {
|
||||
if (summary.sampleCount < 3 ||
|
||||
summary.averageHeartRateBpm == null ||
|
||||
summary.maxHeartRateBpm == null) {
|
||||
return;
|
||||
}
|
||||
if (summary.sessionId.trim().isEmpty ||
|
||||
summary.averageHeartRateBpm! <= 0 ||
|
||||
summary.maxHeartRateBpm! <= 0) {
|
||||
if (!_isUsableHeartRateSummary(summary)) {
|
||||
return;
|
||||
}
|
||||
final sessionId = summary.sessionId.trim();
|
||||
final histories = await repository.listActive();
|
||||
WorkoutHistory? history;
|
||||
for (final candidate in histories) {
|
||||
if (candidate.metadata.id == summary.sessionId ||
|
||||
candidate.sourceActiveWorkoutSessionId == summary.sessionId) {
|
||||
history = candidate;
|
||||
break;
|
||||
final applied = await _applyHeartRateSummary(histories, summary);
|
||||
if (applied) {
|
||||
_pendingHeartRateSummaries.remove(sessionId);
|
||||
return;
|
||||
}
|
||||
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 ||
|
||||
history.averageHeartRateBpm != null ||
|
||||
history.maxHeartRateBpm != null) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
await repository.patchHeartRateSummary(
|
||||
historyId: history.metadata.id,
|
||||
@ -5689,6 +5716,37 @@ final class WorkoutHistoryUseCases {
|
||||
maxHeartRateBpm: summary.maxHeartRateBpm!,
|
||||
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);
|
||||
});
|
||||
|
||||
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(
|
||||
'ActiveWorkoutSensorUseCases tracks live heart rate and calories',
|
||||
() async {
|
||||
|
||||
@ -1,9 +1,32 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
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 {
|
||||
namespace = "com.gametime.watch"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@ -15,16 +38,25 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.gametime.app"
|
||||
applicationId = "com.gametime.watch"
|
||||
minSdk = 30
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("sharedLocal") {
|
||||
storeFile = sharedSigningStoreFile
|
||||
storePassword = requiredSharedSigningProperty("storePassword")
|
||||
keyAlias = requiredSharedSigningProperty("keyAlias")
|
||||
keyPassword = requiredSharedSigningProperty("keyPassword")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("sharedLocal")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -679,10 +679,6 @@ final class _ActiveContent extends StatelessWidget {
|
||||
_SmallLabel(dominantLabel),
|
||||
const SizedBox(height: 2),
|
||||
_DominantValue(dominantValue),
|
||||
if (_heartRateLabel(state.sensorSample) case final heartRate?) ...[
|
||||
const SizedBox(height: 1),
|
||||
_LiveHeartRateLine(heartRate),
|
||||
],
|
||||
if (timer != null) ...[
|
||||
const SizedBox(height: 3),
|
||||
_TimerToggleButton(
|
||||
@ -796,10 +792,6 @@ final class _ManualScoreContent extends StatelessWidget {
|
||||
? const _PendingDot()
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
if (_heartRateLabel(state.sensorSample) case final heartRate?) ...[
|
||||
const SizedBox(height: 1),
|
||||
_LiveHeartRateLine(heartRate),
|
||||
],
|
||||
const SizedBox(height: 5),
|
||||
if (timer != null)
|
||||
_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 {
|
||||
const _RestContent({required this.state, required this.onTogglePause});
|
||||
|
||||
|
||||
@ -212,7 +212,7 @@ void main() {
|
||||
viewModel.dispose();
|
||||
});
|
||||
|
||||
testWidgets('shows live heart rate on session and telemetry on stats page', (
|
||||
testWidgets('shows telemetry only on stats page when available', (
|
||||
tester,
|
||||
) async {
|
||||
final client = _FakeNativeWatchBridgeClient();
|
||||
@ -245,7 +245,7 @@ void main() {
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('142 bpm'), findsOneWidget);
|
||||
expect(find.text('142 bpm'), findsNothing);
|
||||
expect(find.byTooltip('Stats'), findsOneWidget);
|
||||
|
||||
await tester.drag(
|
||||
@ -261,6 +261,7 @@ void main() {
|
||||
|
||||
expect(find.text('Stats'), findsOneWidget);
|
||||
expect(find.text('FC'), findsOneWidget);
|
||||
expect(find.text('142 bpm'), findsOneWidget);
|
||||
expect(find.text('840 m'), findsOneWidget);
|
||||
expect(find.text('186 kcal'), findsOneWidget);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user