mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 19:53:55 +01:00
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Progression and the in-progress session now live at
screen-locker-sync/exercise_state/<name> and .../active_session — siblings of
devices/, not inside it. Both workout readers (_workout_sync.py:113 and
workout_sync_service.dart:307) list only screen-locker-sync/devices, so a
progression record is structurally invisible to them and cannot be miscounted
as a workout, which would silently grant unlock credit. Keyed on exercise name
because a reinstall mints a new per-install uuid.
Startup no longer calls requestStoragePermission(): that threw the user into a
system Settings page on every launch, for a permission that is now genuinely
optional. It only checks; the grant moved to an OFFLINE BACKUP section in
Settings, its sole caller.
The clobber guard keys on a persisted progression_synced_at marker, NOT
looksFreshlyInstalled(). The only production caller (_finishWorkout) writes a
workout_history row and last_workout_type BEFORE it pushes, so a freshness test
was already false by then and could never fire — a reinstall that connected
Firebase late (the uninstall wipes the keystore, so startup's pull finds no
account) would have overwritten every real remote record with factory defaults
on its first finished workout. Connecting in Settings now pulls immediately,
which closes that window. The same marker stops a pull reverting a deliberate
setExerciseWeight/resetExerciseToDefaults, which write no history at all.
Also adds a Dart silent-failure pre-commit hook. The Python one is
types:[python] and had never looked at a single Dart file — in the app where a
swallowed sync error costs the user a workout with nothing anywhere saying so.
It found 21 real silent swallows (keystore reads/writes, /sdcard mirror
failures, the HTTP bind, device-flow polls, and a .catchError((_) {})), all now
logging why.
Verified on the Pixel 6a: reinstall + deny storage + connect late restored
Bench 22.5→20.0, Row 22.5→25.0, Situp 30→31 reps with the remote copy
untouched; the RTDB device list stayed at exactly 3 entries and no progression
leaked into pull_synced_workout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JupKvZCNKRmEeRQY3G1unG
82 lines
2.8 KiB
Dart
82 lines
2.8 KiB
Dart
/// Writes workout result JSON to external storage (ADB) and the HTTP server.
|
|
library;
|
|
|
|
import 'dart:io';
|
|
import 'package:flutter/foundation.dart' show debugPrint;
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:workout_app/models/workout_session.dart';
|
|
import 'package:workout_app/services/http_server_service.dart';
|
|
|
|
/// Path on the phone's external storage where the PC reads workout data.
|
|
const String kSyncFilePath = '/sdcard/workout_result.json';
|
|
|
|
/// Handles writing completed workout sessions to disk and the HTTP server.
|
|
class SyncService {
|
|
/// Writes [session] as JSON to external storage and updates the HTTP server.
|
|
///
|
|
/// Falls back to app-external directory if /sdcard/ is not writable.
|
|
Future<SyncResult> writeWorkoutResult(WorkoutSession session) async {
|
|
final json = session.toJsonString();
|
|
|
|
// Always update the in-app HTTP server so the PC can read via WiFi.
|
|
HttpServerService.instance.latestWorkout = json;
|
|
|
|
// Try the primary path first (/sdcard/ — ADB-accessible without root).
|
|
try {
|
|
final file = File(kSyncFilePath);
|
|
await file.writeAsString(json);
|
|
return const SyncResult(success: true, path: kSyncFilePath);
|
|
} on Exception catch (error) {
|
|
// Fallback: app-specific external directory (still ADB accessible).
|
|
debugPrint(
|
|
'WorkoutApp: could not write $kSyncFilePath ($error) — trying the '
|
|
'app-specific external directory next.',
|
|
);
|
|
}
|
|
|
|
// External storage is Android-only; getExternalStorageDirectory() throws
|
|
// an UnsupportedError (not an Exception) elsewhere, so guard the call.
|
|
// coverage:ignore-start
|
|
// Android-only: getExternalStorageDirectory is unavailable on the Linux
|
|
// test host, so this block can't be exercised in CI.
|
|
if (Platform.isAndroid) {
|
|
try {
|
|
final dir = await getExternalStorageDirectory();
|
|
if (dir != null) {
|
|
final file = File('${dir.path}/workout_result.json');
|
|
await file.writeAsString(json);
|
|
return SyncResult(success: true, path: file.path);
|
|
}
|
|
} on Exception catch (error) {
|
|
// Fallback failed.
|
|
debugPrint(
|
|
'WorkoutApp: the external-storage fallback failed too ($error) — '
|
|
'this workout will NOT be readable over ADB/HTTP.',
|
|
);
|
|
}
|
|
}
|
|
// coverage:ignore-end
|
|
|
|
return const SyncResult(
|
|
success: false,
|
|
path: null,
|
|
error: 'No writable external path',
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Result of a [SyncService.writeWorkoutResult] call.
|
|
class SyncResult {
|
|
/// Creates a sync result.
|
|
const SyncResult({required this.success, required this.path, this.error});
|
|
|
|
/// Whether the write succeeded.
|
|
final bool success;
|
|
|
|
/// Absolute path where the file was written, or null on failure.
|
|
final String? path;
|
|
|
|
/// Human-readable error message on failure.
|
|
final String? error;
|
|
}
|