388 lines
12 KiB
Dart
388 lines
12 KiB
Dart
// lib/model/activity/activity_library_bootstrap.dart
|
||
import 'dart:io';
|
||
|
||
import 'package:aves/model/settings/defaults.dart';
|
||
import 'package:aves/model/settings/settings.dart';
|
||
import 'package:aves/services/common/services.dart';
|
||
import 'package:flutter/material.dart';
|
||
|
||
class ActivityLibraryBootstrap {
|
||
static bool _running = false;
|
||
|
||
static Future<void> ensureConfigured(BuildContext context) async {
|
||
if (_running) return;
|
||
_running = true;
|
||
|
||
try {
|
||
final existingTreeUri = settings.activityRootTreeUri;
|
||
|
||
// Già configurata: non chiedere più nulla.
|
||
// Però proviamo comunque a garantire che esista Activity/main.
|
||
if (existingTreeUri != null && existingTreeUri.isNotEmpty) {
|
||
final mainReady = await storageService.ensureActivityMainDirectory(
|
||
existingTreeUri,
|
||
);
|
||
|
||
if (!mainReady && context.mounted) {
|
||
await _showActivityMainCreationFailedDialog(
|
||
context: context,
|
||
);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
final rootPath = settings.activityRootPath;
|
||
|
||
final createdOrExists = await _tryCreateRootDirectory(rootPath);
|
||
|
||
if (!context.mounted) return;
|
||
|
||
if (createdOrExists) {
|
||
final shouldContinue = await _showActivityRootReadyDialog(
|
||
context: context,
|
||
rootPath: rootPath,
|
||
);
|
||
|
||
if (!shouldContinue) {
|
||
settings.activityRootPromptDone = true;
|
||
return;
|
||
}
|
||
} else {
|
||
final understood = await _showActivityRootMissingDialog(
|
||
context: context,
|
||
rootPath: rootPath,
|
||
);
|
||
|
||
if (!understood) {
|
||
settings.activityRootPromptDone = true;
|
||
return;
|
||
}
|
||
|
||
if (!context.mounted) return;
|
||
|
||
final shouldOpenPicker = await _showOpenPickerDialog(
|
||
context: context,
|
||
rootPath: rootPath,
|
||
);
|
||
|
||
if (!shouldOpenPicker) {
|
||
settings.activityRootPromptDone = true;
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!context.mounted) return;
|
||
|
||
await _pickValidateAndSaveActivityRoot(
|
||
context: context,
|
||
rootPath: rootPath,
|
||
);
|
||
} catch (e, st) {
|
||
debugPrint('[ActivityBootstrap] failed to configure activity library: $e');
|
||
debugPrint('[ActivityBootstrap] stack: $st');
|
||
} finally {
|
||
_running = false;
|
||
}
|
||
}
|
||
|
||
static Future<void> _pickValidateAndSaveActivityRoot({
|
||
required BuildContext context,
|
||
required String rootPath,
|
||
}) async {
|
||
while (context.mounted) {
|
||
// Piccola pausa per lasciare chiudere completamente il dialog Flutter
|
||
// prima di aprire il picker Android SAF.
|
||
await Future.delayed(const Duration(milliseconds: 300));
|
||
|
||
if (!context.mounted) return;
|
||
|
||
final picked = await storageService.pickActivityDirectory();
|
||
|
||
if (picked == null || picked.treeUri.isEmpty) {
|
||
settings.activityRootPromptDone = true;
|
||
debugPrint('[ActivityBootstrap] user did not select activity directory');
|
||
return;
|
||
}
|
||
|
||
final selectedPath = picked.path;
|
||
|
||
debugPrint('[ActivityBootstrap] picked treeUri=${picked.treeUri}');
|
||
debugPrint('[ActivityBootstrap] picked path=$selectedPath');
|
||
|
||
if (!_isExpectedActivityRoot(selectedPath, rootPath)) {
|
||
if (!context.mounted) return;
|
||
|
||
final retry = await _showWrongActivityRootDialog(
|
||
context: context,
|
||
expectedPath: rootPath,
|
||
selectedPath: selectedPath,
|
||
);
|
||
|
||
if (!retry) {
|
||
settings.activityRootPromptDone = true;
|
||
return;
|
||
}
|
||
|
||
if (!context.mounted) return;
|
||
|
||
final shouldOpenPicker = await _showOpenPickerDialog(
|
||
context: context,
|
||
rootPath: rootPath,
|
||
);
|
||
|
||
if (!shouldOpenPicker) {
|
||
settings.activityRootPromptDone = true;
|
||
return;
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
settings.activityRootPath = rootPath;
|
||
settings.activityRootTreeUri = picked.treeUri;
|
||
settings.activityRootPromptDone = true;
|
||
|
||
debugPrint('[ActivityBootstrap] saved activity root path=$rootPath');
|
||
debugPrint('[ActivityBootstrap] saved activity treeUri=${picked.treeUri}');
|
||
|
||
final mainReady = await storageService.ensureActivityMainDirectory(
|
||
picked.treeUri,
|
||
);
|
||
|
||
debugPrint('[ActivityBootstrap] Activity/main ready=$mainReady');
|
||
|
||
if (!mainReady && context.mounted) {
|
||
await _showActivityMainCreationFailedDialog(
|
||
context: context,
|
||
);
|
||
}
|
||
|
||
return;
|
||
}
|
||
}
|
||
|
||
static Future<bool> _tryCreateRootDirectory(String rootPath) async {
|
||
try {
|
||
final dir = Directory(rootPath);
|
||
|
||
if (!await dir.exists()) {
|
||
await dir.create(recursive: true);
|
||
debugPrint('[ActivityBootstrap] attempted to create root directory: $rootPath');
|
||
}
|
||
|
||
final existsAfterCreate = await dir.exists();
|
||
|
||
if (existsAfterCreate) {
|
||
debugPrint('[ActivityBootstrap] root directory exists: $rootPath');
|
||
} else {
|
||
debugPrint('[ActivityBootstrap] root directory still missing after create attempt: $rootPath');
|
||
}
|
||
|
||
return existsAfterCreate;
|
||
} catch (e, st) {
|
||
debugPrint('[ActivityBootstrap] failed to create root directory: $rootPath');
|
||
debugPrint('[ActivityBootstrap] error: $e');
|
||
debugPrint('[ActivityBootstrap] stack: $st');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static String _normalizePath(String path) {
|
||
final trimmed = path.trim();
|
||
return trimmed.endsWith('/') ? trimmed : '$trimmed/';
|
||
}
|
||
|
||
static bool _isExpectedActivityRoot(
|
||
String? selectedPath,
|
||
String expectedPath,
|
||
) {
|
||
if (selectedPath == null || selectedPath.trim().isEmpty) {
|
||
return false;
|
||
}
|
||
|
||
return _normalizePath(selectedPath) == _normalizePath(expectedPath);
|
||
}
|
||
|
||
static Future<bool> _showActivityRootReadyDialog({
|
||
required BuildContext context,
|
||
required String rootPath,
|
||
}) async {
|
||
final result = await showDialog<bool>(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (dialogContext) {
|
||
return AlertDialog(
|
||
title: const Text('Cartella attività'),
|
||
content: Text(
|
||
'Aves deve accedere alla cartella:\n\n'
|
||
'$rootPath\n\n'
|
||
'La cartella esiste già o è stata creata correttamente.\n\n'
|
||
'Nella schermata successiva seleziona questa cartella per permettere '
|
||
'ad Aves di scansionare i file FIT, GPX e TCX.\n\n'
|
||
'Dopo il consenso Aves creerà automaticamente anche la sottocartella:\n\n'
|
||
'$rootPath/main',
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||
child: const Text('Più tardi'),
|
||
),
|
||
TextButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||
child: const Text('Continua'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
|
||
return result ?? false;
|
||
}
|
||
|
||
static Future<bool> _showActivityRootMissingDialog({
|
||
required BuildContext context,
|
||
required String rootPath,
|
||
}) async {
|
||
final result = await showDialog<bool>(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (dialogContext) {
|
||
return AlertDialog(
|
||
title: const Text('Cartella Activity mancante'),
|
||
content: Text(
|
||
'Aves non è riuscita a creare automaticamente la cartella:\n\n'
|
||
'$rootPath\n\n'
|
||
'Questo può succedere su Android moderno per le limitazioni di accesso '
|
||
'allo storage.\n\n'
|
||
'Ora dovrai creare manualmente la cartella "Activity" nella memoria interna '
|
||
'del dispositivo, oppure selezionarla se esiste già.\n\n'
|
||
'Percorso richiesto:\n'
|
||
'/storage/emulated/0/Activity\n\n'
|
||
'Dopo il consenso Aves proverà a creare automaticamente la sottocartella:\n\n'
|
||
'/storage/emulated/0/Activity/main',
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||
child: const Text('Annulla'),
|
||
),
|
||
TextButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||
child: const Text('OK, ho capito'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
|
||
return result ?? false;
|
||
}
|
||
|
||
static Future<bool> _showOpenPickerDialog({
|
||
required BuildContext context,
|
||
required String rootPath,
|
||
}) async {
|
||
final result = await showDialog<bool>(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (dialogContext) {
|
||
return AlertDialog(
|
||
title: const Text('Seleziona cartella Activity'),
|
||
content: Text(
|
||
'Nella schermata successiva seleziona esattamente questa cartella:\n\n'
|
||
'$rootPath\n\n'
|
||
'Se non esiste, creala dal selettore file o da un file manager, poi torna qui '
|
||
'e selezionala.\n\n'
|
||
'Aves userà questa cartella come root della libreria attività.\n\n'
|
||
'Dopo il consenso Aves creerà automaticamente anche:\n\n'
|
||
'$rootPath/main',
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||
child: const Text('Più tardi'),
|
||
),
|
||
TextButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||
child: const Text('Apri selettore cartella'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
|
||
return result ?? false;
|
||
}
|
||
|
||
static Future<bool> _showWrongActivityRootDialog({
|
||
required BuildContext context,
|
||
required String expectedPath,
|
||
required String? selectedPath,
|
||
}) async {
|
||
final result = await showDialog<bool>(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (dialogContext) {
|
||
return AlertDialog(
|
||
title: const Text('Cartella non corretta'),
|
||
content: Text(
|
||
'Hai selezionato una cartella diversa da quella richiesta.\n\n'
|
||
'Cartella selezionata:\n'
|
||
'${selectedPath == null || selectedPath.isEmpty ? 'non riconosciuta' : selectedPath}\n\n'
|
||
'Cartella richiesta:\n'
|
||
'$expectedPath\n\n'
|
||
'Devi creare o selezionare esattamente la cartella "Activity" nella memoria interna del dispositivo.\n\n'
|
||
'Non selezionare "Activity/main", "Download", "Documents" o altre cartelle.',
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||
child: const Text('Più tardi'),
|
||
),
|
||
TextButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||
child: const Text('Riprova'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
|
||
return result ?? false;
|
||
}
|
||
|
||
static Future<void> _showActivityMainCreationFailedDialog({
|
||
required BuildContext context,
|
||
}) async {
|
||
await showDialog<void>(
|
||
context: context,
|
||
barrierDismissible: true,
|
||
builder: (dialogContext) {
|
||
return AlertDialog(
|
||
title: const Text('Sottocartella main non creata'),
|
||
content: Text(
|
||
'Aves ha ottenuto l’accesso alla cartella Activity, ma non è riuscita '
|
||
'a creare automaticamente la sottocartella:\n\n'
|
||
'${SettingsDefaults.activityRootPath}/main\n\n'
|
||
'Puoi crearla manualmente con un file manager.\n\n'
|
||
'La scansione continuerà comunque a funzionare sulla cartella Activity '
|
||
'e sulle sue sottocartelle esistenti.',
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||
child: const Text('OK'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
static Future<void> resetConfiguration() async {
|
||
settings.activityRootTreeUri = null;
|
||
settings.activityRootPromptDone = false;
|
||
settings.activityRootPath = SettingsDefaults.activityRootPath;
|
||
}
|
||
}
|