584 lines
21 KiB
Dart
584 lines
21 KiB
Dart
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:timezone/data/latest.dart' as tz;
|
||
|
||
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';
|
||
import 'package:rogapp/services/api_service.dart';
|
||
|
||
Map<String, dynamic> deviceInfo = {};
|
||
|
||
/*
|
||
void saveGameState() async {
|
||
DestinationController destinationController =
|
||
Get.find<DestinationController>();
|
||
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<DestinationController>();
|
||
IndexController indexController = Get.find<IndexController>();
|
||
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>();
|
||
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<IndexController>();
|
||
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>();
|
||
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;
|
||
await Get.putAsync(() => ApiService().init());
|
||
}
|
||
}
|
||
|
||
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);
|
||
});
|
||
*/
|
||
|
||
try {
|
||
tz.initializeTimeZones();
|
||
|
||
// ApiServiceを初期化
|
||
//await Get.putAsync(() => ApiService().init());
|
||
await initServices();
|
||
|
||
runApp(const ProviderScope(child: MyApp()));
|
||
//runApp(HomePage()); // MyApp()からHomePage()に変更
|
||
//runApp(const MyApp());
|
||
}catch(e, stackTrace){
|
||
print('Error during initialization: $e');
|
||
print('Stack trace: $stackTrace');
|
||
}
|
||
}
|
||
|
||
Future<void> initServices() async {
|
||
print('Starting services ...');
|
||
try {
|
||
await Get.putAsync(() => ApiService().init());
|
||
print('All services started...');
|
||
}catch(e){
|
||
print('Error initializing ApiService: $e');
|
||
}
|
||
|
||
try {
|
||
Get.put(SettingsController());
|
||
print('SettingsController initialized successfully');
|
||
} catch (e) {
|
||
print('Error initializing SettingsController: $e');
|
||
}
|
||
|
||
print('All services started...');
|
||
|
||
}
|
||
|
||
Future<void> 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<String, dynamic> 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<Position>? positionStream;
|
||
bool background=false;
|
||
DateTime lastGPSCollectedTime=DateTime.now();
|
||
String team_name="";
|
||
String event_code="";
|
||
|
||
Future<void> startBackgroundTracking() async {
|
||
if (Platform.isIOS && background==false) {
|
||
|
||
final IndexController indexController = Get.find<IndexController>();
|
||
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<void> 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<void> 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<MyApp> createState() => _MyAppState();
|
||
}
|
||
|
||
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||
// This widget is the root of your application.
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
if (context.mounted) {
|
||
restoreGame();
|
||
}
|
||
WidgetsBinding.instance.addObserver(this);
|
||
|
||
// ウィジェットが構築された後に権限をチェック
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
PermissionController.checkAndRequestPermissions();
|
||
});
|
||
|
||
debugPrint("Start MyAppState...");
|
||
}
|
||
|
||
/*
|
||
void showPermissionRequiredDialog() {
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (BuildContext context) {
|
||
return AlertDialog(
|
||
title: Text('権限が必要です'),
|
||
content: Text('このアプリは機能するために位置情報の権限が必要です。設定で権限を許可してください。'),
|
||
actions: <Widget>[
|
||
TextButton(
|
||
child: Text('設定を開く'),
|
||
onPressed: () {
|
||
openAppSettings();
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
TextButton(
|
||
child: Text('アプリを終了'),
|
||
onPressed: () {
|
||
// アプリを終了
|
||
Navigator.of(context).pop();
|
||
// よりクリーンな終了のために 'flutter_exit_app' のようなプラグインを使用することをお勧めします
|
||
// 今回は単にすべてのルートをポップします
|
||
Navigator.of(context).popUntil((route) => false);
|
||
},
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
*/
|
||
|
||
|
||
@override
|
||
void dispose() {
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
super.dispose();
|
||
}
|
||
|
||
// void saveGameState() async {
|
||
// DestinationController destinationController = Get.find<DestinationController>();
|
||
// 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<LocationController>();
|
||
DestinationController destinationController = Get.find<DestinationController>();
|
||
|
||
//DestinationController destinationController =
|
||
// Get.find<DestinationController>();
|
||
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,
|
||
);
|
||
}
|
||
|
||
|
||
|
||
}
|