screen-locker/stronglift_replacement/workout_app/lib/services/sync_service.dart
Krzysztof kuhy Rudnicki 83d77884a0 Workout screen never moves when a rest starts; a sandbox flavor to prove it on the phone
The rest banner used to be inserted into the column only while a break ran,
so its arrival took height from the exercise list, shifted every tile down
and pushed the last one into scroll range. The strip is now a permanently
reserved 64 dp slot (muted "Rest / 00:00" while idle, Skip keeps its space),
the list is a non-scrolling column that scales down as a whole if it would
not fit (0.96 on the Pixel 6a with four tiles, pinned by a widget test at
the phone's logical size with the status bar simulated), and the activity is
locked to portrait. Verified on the real phone: only the tapped circle's
pixels change when a rest starts.

Verifying that needed a workout screen that today's already-logged workout
did not offer, so the app gained a `sandbox` product flavor beside `daily`:
com.kuhy.workout_app.sandbox installs side by side with its own data, refuses
every network connection at the socket layer, starts no LAN server for the
PC to pull from, keeps its /sdcard files beside the daily build's, wears a
red SANDBOX ribbon, and offers a SANDBOX section in Settings (wipe, mark
today not done, rest length in seconds). MainActivity answers the flavor and
carries a `WorkoutSandbox` logcat trace of every tap, break, save and
navigation. On Linux the same mode is WORKOUT_SANDBOX=1, driven under Xvfb by
scripts/run_sandbox_xvfb.sh. CI builds --flavor daily; .phone-deploy-flavor
tells phone_deploy.sh the same.

Set circles and the home icon buttons carry semantics labels so they can be
addressed by element (and by screen readers).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YbBZqUaNWyhHYfzz6zGC7q
2026-09-20 18:55:08 +02:00

86 lines
3.0 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 {
/// Where the result file is written; the sandbox flavor points this at
/// its own file so the PC's ADB pull never sees a sandbox workout.
static String filePath = kSyncFilePath;
/// 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(filePath);
await file.writeAsString(json);
return SyncResult(success: true, path: filePath);
} on Exception catch (error) {
// Fallback: app-specific external directory (still ADB accessible).
debugPrint(
'WorkoutApp: could not write $filePath ($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;
}