V3-11: offline tile pre-download along a route corridor
Three pure modules: tile_math.dart (tilesForBounds/tilesAlongRoute, hard cap enforced by throwing), tile_cache.dart (FileTileCache with LRU eviction before any write that would exceed the cap), tile_downloader.dart (sequential, rate-limited, cooperatively cancellable, one bad tile doesn't abort the rest). CachedTileProvider wires the cache into flutter_map via a custom ImageProvider and now backs RideMap and RoutePlannerScreen's tile layers, so ordinary viewing write-throughs into the same capped cache. RoutePlannerScreen gained a route-corridor download action (no rectangle-selection UI -- the ticket names the corridor as strictly better and V3-07 already exists to hang it off of), with a count/size confirmation before any request and a cancellable progress dialog. Fixed a real bug before shipping: a StatefulBuilder-based progress dialog would have started a new overlapping download subscription on every single progress tick; moved to a dedicated StatefulWidget that subscribes once in initState. Marked partially done: aeroplane-mode verification on a real device is the ticket's own acceptance criterion and needs a phone this environment doesn't have.
This commit is contained in:
@@ -6,16 +6,19 @@
|
||||
/// instrumented test once wiped a real device's rides.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart' show driftRuntimeOptions;
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../config/config.dart';
|
||||
import '../crash/crash_reporter.dart';
|
||||
import '../data/database.dart';
|
||||
import '../data/route_plan_repository.dart';
|
||||
import '../data/trip_repository.dart';
|
||||
import '../domain/models.dart';
|
||||
import '../crash/crash_reporter.dart';
|
||||
import '../data/route_plan_repository.dart';
|
||||
import '../notification/ride_notification_controller.dart';
|
||||
import '../notification/ride_notification_coordinator.dart';
|
||||
import '../recording/geolocator_location_source.dart';
|
||||
@@ -24,6 +27,8 @@ import '../recording/recording_engine.dart';
|
||||
import '../recording/wakelock_controller.dart';
|
||||
import '../telemetry/live_telemetry.dart';
|
||||
import '../telemetry/telemetry_uploader.dart';
|
||||
import '../tiles/cached_tile_provider.dart';
|
||||
import '../tiles/tile_cache.dart';
|
||||
|
||||
/// The Drift database, opened against app-private storage.
|
||||
///
|
||||
@@ -183,3 +188,41 @@ final routePlanProvider = StreamProvider.autoDispose.family<RoutePlan?, int>(
|
||||
final routeWaypointsProvider = StreamProvider.autoDispose.family<List<Waypoint>, int>(
|
||||
(ref, routeId) => ref.watch(routePlanRepositoryProvider).watchWaypoints(routeId),
|
||||
);
|
||||
|
||||
// --- Offline tiles (V3-11) ---------------------------------------------------
|
||||
|
||||
/// A generous but real cap -- storage growth is explicitly not optional to bound (see
|
||||
/// the ticket's Risks). Not user-configurable in this pass; only whether to clear it.
|
||||
const int defaultTileCacheMaxBytes = 200 * 1024 * 1024;
|
||||
|
||||
/// A [FutureProvider] rather than a plain [Provider]: the cache directory comes from
|
||||
/// `path_provider`, which is inherently async. Every reader (the map, the download
|
||||
/// screen, settings) treats "not loaded yet" as "behave as if there is no cache" rather
|
||||
/// than blocking on it -- see [RideMap.tileProvider] falling back to plain networked
|
||||
/// tiles while this resolves.
|
||||
final tileCacheProvider = FutureProvider<TileCache>((ref) async {
|
||||
final support = await getApplicationSupportDirectory();
|
||||
final cache = FileTileCache(
|
||||
directory: Directory('${support.path}/tiles'),
|
||||
maxBytes: defaultTileCacheMaxBytes,
|
||||
);
|
||||
ref.onDispose(cache.dispose);
|
||||
return cache;
|
||||
});
|
||||
|
||||
/// Built fresh from [tileCacheProvider] on every read rather than cached itself --
|
||||
/// cheap to construct, and it must always wrap whichever [TileCache] instance is
|
||||
/// current.
|
||||
final cachedTileProviderProvider = Provider<CachedTileProvider?>((ref) {
|
||||
final cache = ref.watch(tileCacheProvider).valueOrNull;
|
||||
if (cache == null) return null;
|
||||
return CachedTileProvider(cache: cache);
|
||||
});
|
||||
|
||||
/// Settings' "Offline tiles" section reads this rather than [tileCacheProvider]
|
||||
/// directly, so the number on screen updates the moment `Clear cache` invalidates it --
|
||||
/// see `SettingsScreen`.
|
||||
final tileCacheSizeProvider = FutureProvider<int>((ref) async {
|
||||
final cache = await ref.watch(tileCacheProvider.future);
|
||||
return cache.sizeBytes();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user