iOS GPS 設定によるポップアップ、バックグラウンド処理修正。Android は未確認

This commit is contained in:
2024-05-02 11:51:52 +09:00
parent 9d8f1ef31a
commit 7a97127a19
13 changed files with 515 additions and 52 deletions

View File

@ -1,8 +1,12 @@
import 'dart:async';
import 'dart:io';
//import 'dart:convert';
//import 'dart:developer';
import 'package:rogapp/model/gps_data.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';
@ -23,6 +27,10 @@ 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';
Map<String, dynamic> deviceInfo = {};
@ -114,6 +122,8 @@ void main() async {
//Get.put(LocationController());
requestLocationPermission();
// startMemoryMonitoring(); // 2024-4-8 Akira: メモリ使用量のチェックを開始 See #2810
Get.put(SettingsController()); // これを追加
@ -126,6 +136,21 @@ void main() async {
//runApp(const MyApp());
}
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
/*
@ -192,6 +217,99 @@ void showMemoryWarningDialog() {
}
}
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');
}
}
}
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;
}
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@ -209,8 +327,11 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
restoreGame();
}
WidgetsBinding.instance.addObserver(this);
debugPrint("Start MyAppState...");
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
@ -227,37 +348,69 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@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:
// Foreground に戻った時の処理
debugPrint(" ==(Status Changed)==> RESUMED. フォアグラウンドに戻りました");
locationController.resumePositionStream();
//print("RESUMED");
restoreGame();
// バックグラウンド処理を停止
if (Platform.isIOS && destinationController.isRunningBackgroundGPS) {
stopBackgroundTracking();
destinationController.isRunningBackgroundGPS=false;
destinationController.restartGPS();
} else if (Platform.isAndroid) {
const platform = MethodChannel('location');
platform.invokeMethod('stopLocationService');
}
break;
case AppLifecycleState.inactive:
locationController.resumePositionStream();
//print("INACTIVE");
// アプリが非アクティブになったときに発生します。
// これは、別のアプリやシステムのオーバーレイ(着信通話やアラームなど)によって一時的に中断された状態です。
debugPrint(" ==(Status Changed)==> PAUSED. 非アクティブ処理。");
//locationController.resumePositionStream();
// 追加: フロントエンドのGPS信号のlistenを停止
locationController.stopPositionStream();
if (Platform.isIOS ) { // iOSはバックグラウンドでもフロントの処理が生きている。
destinationController.isRunningBackgroundGPS=true;
startBackgroundTracking();
}else if(Platform.isAndroid){
const platform = MethodChannel('location');
platform.invokeMethod('startLocationService');
}
saveGameState();
break;
case AppLifecycleState.paused:
locationController.resumePositionStream();
//print("PAUSED");
// バックグラウンドに移行したときの処理
//locationController.resumePositionStream();
debugPrint(" ==(Status Changed)==> PAUSED. バックグラウンド処理。");
saveGameState();
break;
case AppLifecycleState.detached:
locationController.resumePositionStream();
//print("DETACHED");
// アプリが終了する直前に発生します。この状態では、アプリはメモリから解放される予定です。
//locationController.resumePositionStream();
debugPrint(" ==(Status Changed)==> DETACHED アプリは終了します。");
saveGameState();
break;
case AppLifecycleState.hidden:
locationController.resumePositionStream();
//print("DETACHED");
// Web用の特殊な状態で、モバイルアプリでは発生しません。
//locationController.resumePositionStream();
debugPrint(" ==(Status Changed)==> Hidden アプリが隠れた");
saveGameState();
break;
}
}
@override
Widget build(BuildContext context) {
return GetMaterialApp(

View File

@ -946,6 +946,7 @@ class DestinationController extends GetxController {
is_checkin: isCheckin,
created_at: DateTime.now().millisecondsSinceEpoch);
var res = await db.insertGps(gps_data);
//debugPrint("Saved GPS data into DB...");
}
} catch (err) {
print("errr ready gps ${err}");
@ -1202,7 +1203,10 @@ class DestinationController extends GetxController {
}
// コントローラーの初期化時に呼び出されるライフサイクルメソッドです。
//
//
bool inError=false;
bool isRunningBackgroundGPS=false;
@override
void onInit() async {
super.onInit();
@ -1225,7 +1229,53 @@ class DestinationController extends GetxController {
handleLocationUpdate(locationMarkerPosition);
//}
}, onError: (err) {
print("Location Error: $err");
if(inError==false){
inError = true;
debugPrint("Location Error: $err");
// エラーが発生した場合、locationMarkerPositionStreamControllerにエラーを追加します。
locationController.locationMarkerPositionStreamController.addError(err);
// ここにエラー発生時の処理を追加します。
if (err is LocationServiceDisabledException) {
// 位置情報サービスが無効になっている場合の処理
print('Location services are disabled');
Get.snackbar(
'エラー',
'位置情報サービスが無効になっています。設定画面から位置情報サービスを有効にして下さい。不明な場合にはエンジニアスタッフにお問い合わせください。',
backgroundColor: Colors.red,
colorText: Colors.white,
duration: Duration(seconds: 3),
);
inError = false;
} else if (err is PermissionDeniedException) {
// 位置情報の権限がない場合の処理
print('Location permissions are denied');
Get.snackbar(
'エラー',
'位置情報サービスが許可されていません。設定画面から岐阜ナビの位置情報サービスを許可して下さい。不明な場合にはエンジニアスタッフにお問い合わせください。',
backgroundColor: Colors.red,
colorText: Colors.white,
duration: Duration(seconds: 3),
);
inError = false;
} else {
// その他のエラーの場合の処理
print('Location Error: $err');
Get.snackbar(
'エラー',
'位置情報サービスに問題が発生しました。位置情報サービスを再起動していますので少しお待ちください。',
backgroundColor: Colors.red,
colorText: Colors.white,
duration: Duration(seconds: 3),
);
// GPSデータのListenを再開する処理を追加
if( isRunningBackgroundGPS==false && inError ) {
restartGPS();
}
}
}
//print("Location Error: $err");
});
startGame();
@ -1233,6 +1283,14 @@ class DestinationController extends GetxController {
checkGPSDataReceived();
}
void restartGPS(){
// GPSデータのListenを再開する処理を追加
Future.delayed(Duration(seconds: 5), () {
locationController.startPositionStream();
inError=false;
});
}
// コントローラーのクローズ時に呼び出されるライフサイクルメソッドです。
//
@override
@ -1248,18 +1306,23 @@ class DestinationController extends GetxController {
//
// 要検討GPSデータの追加に失敗した場合のエラーハンドリングを追加することをお勧めします。
//
double prevLat = 0.0; // 直前の位置
double prevLon = 0.0;
void handleLocationUpdate(LocationMarkerPosition? position) async {
try {
//final DestinationController destinationController = Get.find<DestinationController>();
//final signalStrength = locationController.getGpsSignalStrength();
okToUseGPS = false;
double prevLat = currentLat; // 直前の位置
double prevLon = currentLon;
if (position != null) {
currentLat = position.latitude;
currentLon = position.longitude;
if( prevLat==0.0 ){
prevLat = currentLat;
prevLon = currentLon;
}
lastValidGPSLocation = LatLng(currentLat, currentLon);
okToUseGPS = true;
lastGPSDataReceivedTime = DateTime.now();
@ -1316,7 +1379,8 @@ class DestinationController extends GetxController {
Duration difference = lastGPSCollectedTime.difference(DateTime.now())
.abs();
// 最後にGPS信号を取得した時刻から10秒以上経過、かつ10m以上経過普通に歩くスピード
if (difference.inSeconds >= 10 && distanceToDest >= 10) {
//debugPrint("時間差:${difference.inSeconds}, 距離差:${distanceToDest}");
if (difference.inSeconds >= 10 || distanceToDest >= 30) {
// print(
// "^^^^^^^^ GPS data collected ${DateFormat('kk:mm:ss \n EEE d MMM').format(DateTime.now())}, ^^^ ${position.latitude}, ${position.longitude}");
@ -1333,11 +1397,14 @@ class DestinationController extends GetxController {
if (isInRog.value) {
await addGPStoDB(position.latitude, position.longitude);
lastGPSCollectedTime = DateTime.now();
prevLat = position.latitude;
prevLon = position.longitude;
debugPrint("フロントエンドでのGPS保存(時間差:${difference.inSeconds}, 距離差:${distanceToDest}) : Time=${lastGPSCollectedTime}");
}
}
}
} catch(e) {
debugPrint("Error: ${e}");
debugPrint("handleLocationUpdate Error: ${e}");
} finally {
/* Akira , 2024-4-5
if (position != null &&

View File

@ -72,6 +72,7 @@ class _GpsPageState extends State<GpsPage> {
],
)),
),
/*
Container(
color: Colors.transparent,
child: i.is_checkin == 1
@ -93,6 +94,8 @@ class _GpsPageState extends State<GpsPage> {
color: Colors.black,
))
: Container()),
*/
],
);
}

View File

@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
import 'package:geojson_vi/geojson_vi.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:rogapp/model/destination.dart';
@ -120,11 +121,76 @@ class IndexController extends GetxController {
}
}
Future<void> _checkLocationPermission() async {
if (Get.context == null) {
debugPrint('Get.context is null in _checkLocationPermission');
return;
}
LocationPermission permission = await Geolocator.checkPermission();
//permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
debugPrint('GPS : Denied');
await showLocationPermissionDeniedDialog();
} else if (permission == LocationPermission.deniedForever) {
debugPrint('GPS : Denied forever');
await showLocationPermissionDeniedDialog();
}else if (permission == LocationPermission.whileInUse){
debugPrint('GPS : While-In-Use');
await showLocationPermissionDeniedDialog();
}else{
debugPrint("Permission is no problem....");
}
}
// 追加
Future<void> showLocationPermissionDeniedDialog() async {
if (Get.context != null) {
print('Showing location permission denied dialog');
await showDialog(
context: Get.context!,
barrierDismissible: false,
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: AlertDialog(
title: Text('位置情報の許可が必要です'),
content: Text('設定>プライバシーとセキュリティ>位置情報サービス を開いて、岐阜ナビを探し、「位置情報の許可」を「常に」にして下さい。'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('OK'),
),
],
),
);
},
);
} else {
print('Get.context is null in showLocationPermissionDeniedDialog');
// Get.contextがnullの場合の処理
print('Location permission denied, but context is null');
}
}
@override
void onInit() {
_connectivitySubscription =
_connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
super.onInit();
print('IndexController onInit called'); // デバッグ用の出力を追加
}
void checkPermission()
{
debugPrint("MapControllerの初期化が完了したら、位置情報の許可をチェックする");
_checkLocationPermission();
}
@override
@ -425,9 +491,11 @@ class IndexController extends GetxController {
// 2024-04-03 Akira .. Update the code . See ticket 2800.
//
// 2024-4-8 Akira : See 2809
// IndexControllerクラスでは、Future.delayedの呼び出しをunawaitedで囲んで、非同期処理の結果を待たずに先に進むようにしました。これにより、メモリリークを防ぐことができます
// IndexControllerクラスでは、Future.delayedの呼び出しをunawaitedで囲んで、
// 非同期処理の結果を待たずに先に進むようにしました。これにより、メモリリークを防ぐことができます
//
// 要検討Future.delayedを使用して非同期処理を待たずに先に進むようにしていますが、これによってメモリリークが発生する可能性があります。非同期処理の結果を適切に処理することを検討してください。
// 要検討Future.delayedを使用して非同期処理を待たずに先に進むようにしていますが、
// これによってメモリリークが発生する可能性があります。非同期処理の結果を適切に処理することを検討してください。
//
void loadLocationsBound() async {
if (isCustomAreaSelected.value == true) {
@ -437,11 +505,6 @@ class IndexController extends GetxController {
// MapControllerの初期化が完了するまで待機
await waitForMapControllerReady();
// Akira 追加:2024-4-6 #2800
//await waitForMapControllerReady(); // MapControllerの初期化が完了するまで待機
// Akira 追加:2024-4-6 #2800
// ==> remove 2024-4-8
locations.clear();
String cat = currentCat.isNotEmpty ? currentCat[0] : "";
if (currentCat.isNotEmpty && currentCat[0] == "-all-") {
@ -458,14 +521,16 @@ class IndexController extends GetxController {
*/
LatLngBounds bounds = mapController.bounds!;
if (bounds == null) {
// MapControllerの初期化が完了していない場合は処理を行わない
return;
}
currentBound.clear();
currentBound.add(bounds);
isLoading.value = true; // ローディング状態をtrueに設定
// unawaited( Future.delayed(const Duration(seconds: 1), () async {
// remove
//print("bounds --- (${bounds.southWest.latitude},${bounds.southWest.longitude}),(${bounds.northWest.latitude},${bounds.northWest.longitude}),(${bounds.northEast.latitude},${bounds.northEast.longitude}),(${bounds.southEast.latitude},${bounds.southEast.longitude})");
// 要検討APIからのレスポンスがnullの場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。
@ -481,6 +546,7 @@ class IndexController extends GetxController {
bounds.southEast.longitude,
cat
);
/*
if (value == null) {
// APIからのレスポンスがnullの場合
print("LocationService.loadLocationsBound からの回答がnullのため、マップをリロード");
@ -491,8 +557,33 @@ class IndexController extends GetxController {
} // 追加
return;
}
*/
isLoading.value = false; // ローディング状態をfalseに設定
if (value == null) {
// APIからのレスポンスがnullの場合
print("LocationService.loadLocationsBound からの回答がnullです");
} else {
if (value.features.isEmpty) {
if (showPopup == false) {
return;
}
Get.snackbar(
"Too many Points",
"please zoom in",
backgroundColor: Colors.yellow,
colorText: Colors.white,
icon: const Icon(Icons.assistant_photo_outlined, size: 40.0, color: Colors.blue),
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
);
showPopup = false;
}
if (value.features.isNotEmpty) {
locations.add(value);
}
}
/*
if (value != null && value.features.isEmpty) {
if (showPopup == false) {
return;
@ -513,6 +604,7 @@ class IndexController extends GetxController {
if (value != null && value.features.isNotEmpty) {
locations.add(value);
}
*/
} catch ( e) {
print("Error in loadLocationsBound: $e");
// エラーが発生した場合のリトライ処理や適切なエラーメッセージの表示を行う

View File

@ -104,6 +104,7 @@ class IndexPage extends GetView<IndexController> {
// タップすることでGPS信号の強弱をシミュレーションできるようにする
// Akira 2024-4-5
//
/*
Obx(() {
if (locationController.isSimulationMode) {
return DropdownButton<String>(
@ -129,6 +130,8 @@ class IndexPage extends GetView<IndexController> {
);
}
}),
*/
/*
Obx(() => locationController.isSimulationMode
? DropdownButton<String>(

View File

@ -49,7 +49,7 @@ class LocationController extends GetxController {
//===== Akira Added 2024-4-9 start
// GPSシミュレーション用の変数を追加 ===> 本番では false にする。
bool isSimulationMode = true;
bool isSimulationMode = false;
// GPS信号強度をシミュレートするための変数
final Rx<String> _simulatedSignalStrength = Rx<String>('low');
@ -93,6 +93,7 @@ class LocationController extends GetxController {
return 'low';
}
final accuracy = position.accuracy;
//debugPrint("getGpsSignalStrength : ${accuracy}");
if(isSimulationMode){
return _simulatedSignalStrength.value; // GPS信号強度シミュレーション
}else {
@ -346,6 +347,7 @@ class LocationController extends GetxController {
}
}
void handleLocationUpdate(LocationMarkerPosition? position) async {
if (position != null) {
//debugPrint("position = ${position}");

View File

@ -70,9 +70,12 @@ class _MapWidgetState extends State<MapWidget> with WidgetsBindingObserver {
// MapControllerの初期化が完了するまで待機
WidgetsBinding.instance.addPostFrameCallback((_) {
debugPrint("MapControllerの初期化が完了");
setState(() {
indexController.isMapControllerReady.value = true;
});
// MapControllerの初期化が完了したら、IndexControllerのonInitを呼び出す
indexController.checkPermission();
});
late MapResetController mapResetController = MapResetController();