import 'dart:async'; import 'dart:io'; //import 'dart:convert'; //import 'dart:developer'; import 'package:rogapp/model/gps_data.dart'; import 'package:rogapp/pages/home/home_page.dart'; import 'package:rogapp/utils/database_gps.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; //import 'package:vm_service/vm_service.dart'; //import 'package:dart_vm_info/dart_vm_info.dart'; import 'package:rogapp/pages/settings/settings_controller.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_binding.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/utils/location_controller.dart'; import 'package:rogapp/utils/string_values.dart'; import 'package:rogapp/widgets/debug_widget.dart'; import 'package:shared_preferences/shared_preferences.dart'; // import 'package:is_lock_screen/is_lock_screen.dart'; import 'package:rogapp/services/device_info_service.dart'; import 'package:rogapp/services/error_service.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; //import 'dart:async'; //import 'package:get/get.dart'; import 'package:flutter/services.dart'; import 'package:permission_handler/permission_handler.dart'; import 'pages/permission/permission.dart'; Map deviceInfo = {}; /* void saveGameState() async { DestinationController destinationController = Get.find(); SharedPreferences pref = await SharedPreferences.getInstance(); pref.setBool("is_in_rog", destinationController.isInRog.value); pref.setBool( "rogaining_counted", destinationController.rogainingCounted.value); pref.setBool("ready_for_goal", DestinationController.ready_for_goal); } */ // 現在のユーザーのIDも一緒に保存するようにします。 void saveGameState() async { DestinationController destinationController = Get.find(); IndexController indexController = Get.find(); SharedPreferences pref = await SharedPreferences.getInstance(); debugPrint("indexController.currentUser = ${indexController.currentUser}"); if(indexController.currentUser.isNotEmpty) { pref.setInt("user_id", indexController.currentUser[0]["user"]["id"]); }else{ debugPrint("User is empty...."); } pref.setBool("is_in_rog", destinationController.isInRog.value); pref.setBool( "rogaining_counted", destinationController.rogainingCounted.value); pref.setBool("ready_for_goal", DestinationController.ready_for_goal); } /* void restoreGame() async { SharedPreferences pref = await SharedPreferences.getInstance(); DestinationController destinationController = Get.find(); destinationController.skipGps = false; destinationController.isInRog.value = pref.getBool("is_in_rog") ?? false; destinationController.rogainingCounted.value = pref.getBool("rogaining_counted") ?? false; DestinationController.ready_for_goal = pref.getBool("ready_for_goal") ?? false; //print( // "--restored -- destinationController.isInRog.value ${pref.getBool("is_in_rog")} -- ${pref.getBool("rogaining_counted")}"); } */ void restoreGame() async { SharedPreferences pref = await SharedPreferences.getInstance(); IndexController indexController = Get.find(); int? savedUserId = pref.getInt("user_id"); //int? currUserId = indexController.currentUser[0]['user']['id']; //debugPrint("savedUserId=${savedUserId}, currentUser=${currUserId}"); if (indexController.currentUser.isNotEmpty && indexController.currentUser[0]["user"]["id"] == savedUserId) { DestinationController destinationController = Get.find(); destinationController.skipGps = false; destinationController.isInRog.value = pref.getBool("is_in_rog") ?? false; destinationController.rogainingCounted.value = pref.getBool("rogaining_counted") ?? false; DestinationController.ready_for_goal = pref.getBool("ready_for_goal") ?? false; } } void main() async { WidgetsFlutterBinding.ensureInitialized(); await FlutterMapTileCaching.initialise(); final StoreDirectory instanceA = FMTC.instance('OpenStreetMap (A)'); await instanceA.manage.createAsync(); await instanceA.metadata.addAsync( key: 'sourceURL', value: 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', ); await instanceA.metadata.addAsync( key: 'validDuration', value: '14', ); await instanceA.metadata.addAsync( key: 'behaviour', value: 'cacheFirst', ); deviceInfo = await DeviceInfoService.getDeviceInfo(); FlutterError.onError = (FlutterErrorDetails details) { FlutterError.presentError(details); Get.log('Flutter error: ${details.exception}'); Get.log('Stack trace: ${details.stack}'); ErrorService.reportError(details.exception, details.stack ?? StackTrace.current, deviceInfo, LogManager().operationLogs); }; //Get.put(LocationController()); //await PermissionController.checkAndRequestPermissions(); //requestLocationPermission(); // startMemoryMonitoring(); // 2024-4-8 Akira: メモリ使用量のチェックを開始 See #2810 Get.put(SettingsController()); // これを追加 /* runZonedGuarded(() { runApp(const ProviderScope(child: MyApp())); }, (error, stackTrace) { ErrorService.reportError(error, stackTrace, deviceInfo); }); */ runApp(const ProviderScope(child: MyApp())); //runApp(HomePage()); // MyApp()からHomePage()に変更 //runApp(const MyApp()); } Future requestLocationPermission() async { try { final status = await Permission.locationAlways.request(); if (status == PermissionStatus.granted) { print('Location permission granted'); } else { print('Location permission denied'); //await showLocationPermissionDeniedDialog(); // 追加 } } catch (e) { print('Error requesting location permission: $e'); } } // メモリ使用量の解説:https://qiita.com/hukusuke1007/items/e4e987836412e9bc73b9 /* // 2024-4-8 Akira: メモリ使用量のチェックのため追加 See #2810 // startMemoryMonitoring関数が5分ごとに呼び出され、メモリ使用量をチェックします。 // メモリ使用量が閾値(ここでは500MB)を超えた場合、エラーメッセージとスタックトレースをレポートします。 // void startMemoryMonitoring() { const threshold = 500 * 1024 * 1024; // 500MB // メモリ使用量情報を取得 final memoryUsage = MemoryUsage.fromJson(DartVMInfo.getAllocationProfile()); if (memoryUsage.heapUsage > threshold) { final now = DateTime.now().toIso8601String(); final message = 'High memory usage detected at $now: ${memoryUsage.heapUsage} bytes'; print(message); reportError(message, StackTrace.current); showMemoryWarningDialog(); } Timer(const Duration(minutes: 5), startMemoryMonitoring); } class MemoryUsage { final int heapUsage; MemoryUsage({required this.heapUsage}); factory MemoryUsage.fromJson(Map json) { return MemoryUsage( heapUsage: json['heapUsage'] as int, ); } } */ // 2024-4-8 Akira: メモリ使用量のチェックのため追加 See #2810 // reportError関数でエラーレポートを送信します。具体的な実装は、利用するエラー報告サービスによって異なります。 void reportError(String message, StackTrace stackTrace) async { // エラーレポートの送信処理を実装 // 例: SentryやFirebase Crashlyticsなどのエラー報告サービスを利用 print("ReportError : ${message} . stacktrace : ${stackTrace}"); } // 2024-4-8 Akira: メモリ使用量のチェックのため追加 See #2810 // showMemoryWarningDialog関数で、メモリ使用量が高い場合にユーザーに警告ダイアログを表示します。 // void showMemoryWarningDialog() { if (Get.context != null) { showDialog( context: Get.context!, builder: (context) => AlertDialog( title: const Text('メモリ使用量の警告'), content: const Text('アプリのメモリ使用量が高くなっています。アプリを再起動することをお勧めします。'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('OK'), ), ], ), ); } } StreamSubscription? positionStream; bool background=false; DateTime lastGPSCollectedTime=DateTime.now(); String team_name=""; String event_code=""; Future startBackgroundTracking() async { if (Platform.isIOS && background==false) { final IndexController indexController = Get.find(); if(indexController.currentUser.length>0) { team_name = indexController.currentUser[0]["user"]['team_name']; event_code = indexController.currentUser[0]["user"]["event_code"]; } background = true; debugPrint("バックグラウンド処理を開始しました。"); final LocationSettings locationSettings = LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 100, ); try { positionStream = Geolocator.getPositionStream(locationSettings: locationSettings) .listen((Position? position) async { if (position != null) { final lat = position.latitude; final lng = position.longitude; //final timestamp = DateTime.now(); final accuracy = position.accuracy; // GPS信号強度がlowの場合はスキップ if (accuracy > 100) { debugPrint("GPS signal strength is low. Skipping data saving."); return; } Duration difference = lastGPSCollectedTime.difference(DateTime.now()) .abs(); // 最後にGPS信号を取得した時刻から10秒以上経過、かつ10m以上経過(普通に歩くスピード) //debugPrint("時間差:${difference}"); if (difference.inSeconds >= 10 ) { debugPrint("バックグラウンドでのGPS取得時の処理(10secおき) count=${difference.inSeconds}, time=${DateTime.now()}"); // DBにGPSデータを保存 pages/destination/destination_controller.dart await addGPStoDB(lat, lng); lastGPSCollectedTime = DateTime.now(); } } }, onError: (error) { if (error is LocationServiceDisabledException) { print('Location services are disabled'); } else if (error is PermissionDeniedException) { print('Location permissions are denied'); } else { print('Location Error: $error'); } }); } catch (e) { print('Error starting background tracking: $e'); } }else if (Platform.isAndroid && background == false) { background = true; debugPrint("バックグラウンド処理を開始しました。"); try { // 位置情報の権限が許可されているかを確認 await PermissionController.checkAndRequestPermissions(); }catch(e){ print('Error starting background tracking: $e'); } } } Future addGPStoDB(double la, double ln) async { //debugPrint("in addGPStoDB ${indexController.currentUser}"); GpsDatabaseHelper db = GpsDatabaseHelper.instance; try { GpsData gps_data = GpsData( id: 0, team_name: team_name, event_code: event_code, lat: la, lon: ln, is_checkin: 0, created_at: DateTime.now().millisecondsSinceEpoch); var res = await db.insertGps(gps_data); //debugPrint("バックグラウンドでのGPS保存:"); } catch (err) { print("errr ready gps ${err}"); return; } } Future stopBackgroundTracking() async { if (Platform.isIOS && background==true) { background=false; debugPrint("バックグラウンド処理:停止しました。"); await positionStream?.cancel(); positionStream = null; }else if(Platform.isAndroid && background==true){ background=false; debugPrint("バックグラウンド処理:停止しました。"); const platform = MethodChannel('location'); try { await platform.invokeMethod('stopLocationService'); } on PlatformException catch (e) { print("Failed to stop location service: '${e.message}'."); } } } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _MyAppState(); } class _MyAppState extends State with WidgetsBindingObserver { // This widget is the root of your application. @override void initState() { super.initState(); if (context.mounted) { restoreGame(); } WidgetsBinding.instance.addObserver(this); // Add to clear WidgetsBinding.instance.addPostFrameCallback((_) { showLocationDisclosure(context); }); /* WidgetsBinding.instance.addPostFrameCallback((_) async { await PermissionController.checkAndRequestPermissions(); }); */ debugPrint("Start MyAppState..."); } void showLocationDisclosure(BuildContext context) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: Text('位置情報の使用について'), content: const SingleChildScrollView( child: ListBody( children: [ Text('このアプリでは、以下の目的で位置情報を使用します:'), Text('• チェックポイントの自動チェックイン(アプリが閉じているときも含む)'), Text('• 移動履歴の記録(バックグラウンドでも継続)'), Text('• 現在地周辺の情報表示'), Text('\nバックグラウンドでも位置情報を継続的に取得します。'), Text('これにより、バッテリーの消費が増加する可能性があります。'), ], ), ), actions: [ TextButton( child: const Text('同意しない'), onPressed: () { Navigator.of(context).pop(); // アプリを終了するなどの処理 }, ), TextButton( child: const Text('同意する'), onPressed: () { Navigator.of(context).pop(); requestLocationPermission(); }, ), ], ); }, ); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } // void saveGameState() async { // DestinationController destinationController = Get.find(); // SharedPreferences pref = await SharedPreferences.getInstance(); // pref.setBool("is_in_rog", destinationController.is_in_rog.value); // pref.setBool("rogaining_counted", destinationController.rogaining_counted.value); // } @override void didChangeAppLifecycleState(AppLifecycleState state) { LocationController locationController = Get.find(); DestinationController destinationController = Get.find(); //DestinationController destinationController = // Get.find(); switch (state) { case AppLifecycleState.resumed: // バックグラウンド処理を停止 if (Platform.isIOS && destinationController.isRunningBackgroundGPS) { // Foreground に戻った時の処理 debugPrint(" ==(Status Changed)==> RESUMED. フォアグラウンドに戻りました"); locationController.resumePositionStream(); //print("RESUMED"); restoreGame(); stopBackgroundTracking(); destinationController.isRunningBackgroundGPS=false; destinationController.restartGPS(); } else if (Platform.isAndroid ) { if( destinationController.isRunningBackgroundGPS ){ const platform = MethodChannel('location'); platform.invokeMethod('stopLocationService'); destinationController.isRunningBackgroundGPS=false; destinationController.restartGPS(); debugPrint("stopped android location service.."); } debugPrint("==(Status Changed)==> RESUMED. android フォアグラウンドに戻りました"); locationController.resumePositionStream(); //print("RESUMED"); restoreGame(); }else{ debugPrint("==(Status Changed)==> RESUMED 不明状態"); } break; case AppLifecycleState.inactive: // アプリが非アクティブになったときに発生します。 if (Platform.isIOS && !destinationController.isRunningBackgroundGPS) { // iOSはバックグラウンドでもフロントの処理が生きている。 // これは、別のアプリやシステムのオーバーレイ(着信通話やアラームなど)によって一時的に中断された状態です。 debugPrint(" ==(Status Changed)==> INACTIVE. 非アクティブ処理。"); //locationController.resumePositionStream(); // 追加: フロントエンドのGPS信号のlistenを停止 locationController.stopPositionStream(); destinationController.isRunningBackgroundGPS=true; startBackgroundTracking(); }else if(Platform.isAndroid && !destinationController.isRunningBackgroundGPS){ debugPrint(" ==(Status Changed)==> INACTIVE. 非アクティブ処理。"); }else{ debugPrint("==(Status Changed)==> INACTIVE 不明状態"); } saveGameState(); break; case AppLifecycleState.paused: // バックグラウンドに移行したときの処理 //locationController.resumePositionStream(); debugPrint(" ==(Status Changed)==> PAUSED. バックグラウンド処理。"); if (Platform.isIOS && !destinationController.isRunningBackgroundGPS) { debugPrint("iOS already running background GPS processing when it's inactive"); } else if(Platform.isAndroid && !destinationController.isRunningBackgroundGPS) { debugPrint( " ==(Status Changed)==> PAUSED. Android バックグラウンド処理。"); locationController.stopPositionStream(); const platform = MethodChannel('location'); platform.invokeMethod('startLocationService'); //platform.invokeMethod('stopLocationService'); destinationController.isRunningBackgroundGPS = true; //startBackgroundTracking(); } saveGameState(); break; case AppLifecycleState.detached: // アプリが終了する直前に発生します。この状態では、アプリはメモリから解放される予定です。 //locationController.resumePositionStream(); debugPrint(" ==(Status Changed)==> DETACHED アプリは終了します。"); saveGameState(); break; case AppLifecycleState.hidden: // Web用の特殊な状態で、モバイルアプリでは発生しません。 //locationController.resumePositionStream(); debugPrint(" ==(Status Changed)==> Hidden アプリが隠れた"); saveGameState(); break; } } @override Widget build(BuildContext context) { return GetMaterialApp( translations: StringValues(), locale: const Locale('ja', 'JP'), //locale: const Locale('en', 'US'), fallbackLocale: const Locale('en', 'US'), title: 'ROGAINING', theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color.fromARGB(255, 36, 135, 221)), useMaterial3: true, ), debugShowCheckedModeBanner: false, defaultTransition: Transition.cupertino, opaqueRoute: Get.isOpaqueRouteDefault, popGesture: Get.isPopGestureEnable, transitionDuration: const Duration(milliseconds: 230), initialBinding: IndexBinding(), //HomeBinding(), initialRoute: AppPages.PERMISSION, getPages: AppPages.routes, enableLog: true, ); } }