diff --git a/android/app/build.gradle b/android/app/build.gradle index e6563eb..fe5af34 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -60,6 +60,15 @@ android { signingConfig signingConfigs.debug } } + + splits { + abi { + enable true + reset() + include 'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'mips', 'mips64', 'arm64-v8a' + universalApk true + } + } } flutter { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 581886f..58143ab 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,7 +11,9 @@ android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustResize" + android:showWhenLocked="true" + android:turnScreenOn="true"> + diff --git a/android/app/src/main/res/raw/simple_alarm_music.mp3 b/android/app/src/main/res/raw/simple_alarm_music.mp3 new file mode 100644 index 0000000..ee35837 Binary files /dev/null and b/android/app/src/main/res/raw/simple_alarm_music.mp3 differ diff --git a/lib/main.dart b/lib/main.dart index 202509b..2947234 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'model/ringing_alarm_model.dart'; +import 'view/ringing_alarm_view.dart'; -void main() { +void main() async { + setupRingingAlarm(); runApp(const MyApp()); } @@ -102,10 +105,25 @@ class _MyHomePageState extends State { '$_counter', style: Theme.of(context).textTheme.headline4, ), + FloatingActionButton( + //RingingAlarmPageに飛ぶボタン + heroTag: 'ringing_alarm_page_button', + onPressed: () async { + // "push"で新規画面に遷移 + await Navigator.of(context).push( + MaterialPageRoute(builder: (context) { + return const RingingAlarmTestPage( + title: 'Ringing Alarm Test Page'); + }), + ); + }, + child: const Icon(Icons.alarm), + ), ], ), ), floatingActionButton: FloatingActionButton( + heroTag: 'increment_button', onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), diff --git a/lib/model/ringing_alarm_model.dart b/lib/model/ringing_alarm_model.dart new file mode 100644 index 0000000..8dc70b5 --- /dev/null +++ b/lib/model/ringing_alarm_model.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:intl/intl.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:timezone/data/latest.dart' as tz; +import 'package:timezone/timezone.dart' as tz; + +/// RingingAlarm の初期化をする。mainで呼ぶ。 +void setupRingingAlarm() { + _setupTimeZone(); +} + +// タイムゾーンを設定する +Future _setupTimeZone() async { + tz.initializeTimeZones(); + var tokyo = tz.getLocation('Asia/Tokyo'); + tz.setLocalLocation(tokyo); +} + +///ミリ秒まで表示するフォーマッター +///- 返り値は hh:mm:ss.x の形の String +String formatTime(DateTime dt) { + return "${DateFormat.Hms().format(dt)}.${dt.millisecond.toString().padRight(2, '0').substring(0, 1)}"; +} + +///アラームの管理をするクラス +class RingingAlarm { + final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + Timer? _timer; // タイマーオブジェクト + DateTime _time = DateTime.utc(0, 0, 0) + .add(const Duration(seconds: 10)); // タイマーで管理している時間。10秒をカウントダウンする設定 + bool _isTimerActive = false; // バックグラウンドに遷移した際にタイマーが起動中かどうか + DateTime? _pausedTime; // バックグラウンドに遷移した時間 + int? _notificationId; // 通知ID + bool _notificationOnceFlag = false; //連続して通知ができないようにするフラグ + + DateTime get displayTime => _time; + + ///コンストラクタ + RingingAlarm(); + + /// アプリがバックグラウンドに遷移した際のハンドラ + void handleOnPaused() { + if (_timer != null && _timer!.isActive) { + _isTimerActive = true; + _timer!.cancel(); // タイマーを停止する + } + _pausedTime = DateTime.now(); // バックグラウンドに遷移した時間を記録 + if (_isTimerActive) { + if (_notificationOnceFlag) { + _notificationOnceFlag = false; + if (_time + .difference(DateTime.utc(0, 0, 0)) + .compareTo(const Duration(seconds: 1)) > + 0) { + //タイマーの停止直後に再度通知を作りたがるので間隔を制限 + _notificationId = _scheduleLocalNotification( + _time.difference(DateTime.utc(0, 0, 0))); // ローカル通知をスケジュール登録 + } + } + } + } + + // フォアグラウンドに復帰した時のハンドラ + void handleOnResumed() { + if (_isTimerActive == false) return; // タイマーが動いてなければ何もしない + Duration backgroundDuration = + DateTime.now().difference(_pausedTime!); // バックグラウンドでの経過時間 + // バックグラウンドでの経過時間が終了予定を超えていた場合(この場合は通知実行済みのはず) + if (_time.difference(DateTime.utc(0, 0, 0)).compareTo(backgroundDuration) < + 0) { + _time = DateTime.utc(0, 0, 0); // 時間をリセットする + } else { + _time = _time.add(-backgroundDuration); // バックグラウンド経過時間分時間を進める + startTimer(); // タイマーを再開する + } + } + + //鳴っている通知を消す(正確には最新の通知) + void resetNotification() { + if (_notificationId != null) { + flutterLocalNotificationsPlugin.cancel(_notificationId!); // 通知をキャンセル + } + _isTimerActive = false; // リセット + _notificationId = null; // リセット + _pausedTime = null; + _notificationOnceFlag = false; + } + + // タイマーを開始する + void startTimer() { + _notificationOnceFlag = true; + _timer = Timer.periodic(const Duration(milliseconds: 100), (Timer timer) { + _time = _time.add(const Duration(milliseconds: -100)); + _handleTimeIsOver(); + }); // 1秒ずつ時間を減らす + } + + // タイマーを初期化してから開始する + void restartTimer() { + if (_timer != null) _timer!.cancel(); + _time = DateTime.utc(0, 0, 0); + _time = _time.add(const Duration(seconds: 10)); + startTimer(); + } + + // 時間がゼロになったらタイマーを止める + void _handleTimeIsOver() { + if (_timer != null && + _timer!.isActive && + //_time != null && + _time == DateTime.utc(0, 0, 0)) { + _timer!.cancel(); + _notificationOnceFlag = false; + } + } + + //タイマーを止める + void cancelTimer() { + if (_timer != null && _timer!.isActive) { + _timer!.cancel(); + } + } + + /// タイマー終了をローカル通知。実質メイン + int _scheduleLocalNotification(Duration duration) { + flutterLocalNotificationsPlugin.initialize( + const InitializationSettings( + android: + AndroidInitializationSettings('@mipmap/ic_launcher'), //通知アイコン + iOS: IOSInitializationSettings()), + ); + int notificationId = + DateTime.now().hashCode; //現在時刻から生成しているが、通知を管理するIDを指定できる + DateTime tzDT = tz.TZDateTime.now(tz.local).add(duration); + flutterLocalNotificationsPlugin.zonedSchedule( + notificationId, //通知のID + 'Time is over', //通知のタイトル + 'Wake up! It\'s ${formatTime(tzDT)}', //通知の本文 + tz.TZDateTime.now(tz.local).add(duration), //通知の予約時間 + const NotificationDetails( + android: AndroidNotificationDetails( + 'your channel id', 'your channel name', + channelDescription: 'your channel description', + importance: Importance.high, + priority: Priority.high, + //https://esffects.net/361.html からとってきた音源。 + //alarm2022\android\app\src\main\res\raw\simple_alarm_music.mp3 に配置。 + sound: + RawResourceAndroidNotificationSound('simple_alarm_music'), + fullScreenIntent: true), + iOS: IOSNotificationDetails()), + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + androidAllowWhileIdle: true); + debugPrint("■■■Scheduled for ${formatTime(tzDT)}"); + debugPrint("■■■ from ${formatTime(tz.TZDateTime.now(tz.local))}"); + return notificationId; + } +} diff --git a/lib/view/ringing_alarm_view.dart b/lib/view/ringing_alarm_view.dart new file mode 100644 index 0000000..6a12246 --- /dev/null +++ b/lib/view/ringing_alarm_view.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import '../model/ringing_alarm_model.dart'; + +//新しく作り直すと考えTestとつけている +class RingingAlarmTestPage extends StatefulWidget { + const RingingAlarmTestPage({Key? key, this.title}) : super(key: key); + final String? title; + @override + // ignore: library_private_types_in_public_api + _RingingAlarmTestPageState createState() => _RingingAlarmTestPageState(); +} + +class _RingingAlarmTestPageState extends State + with WidgetsBindingObserver { + //modelの方の実体 + final RingingAlarm _ringingAlarm = RingingAlarm(); + //現在時刻表示用のTimer + Timer? _clockTimer; + + //ページ遷移時にlogがバグったので。 + //参照:https://stackoverflow.com/questions/49340116/setstate-called-after-dispose + @override + void setState(fn) { + if (mounted) { + super.setState(fn); + } + } + + /// 初期化処理 + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + //現在時刻表示用だが画面の更新処理も担っている + _clockTimer = + Timer.periodic(const Duration(milliseconds: 100), (Timer clockTimer) { + setState(() {}); + }); + } + + /// ライフサイクルが変更された際に呼び出される関数をoverrideして、変更を検知 + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + // バックグラウンドに遷移した時 + setState(() { + _ringingAlarm.handleOnPaused(); + }); + } else if (state == AppLifecycleState.resumed) { + // フォアグラウンドに復帰した時 + setState(() { + _ringingAlarm.handleOnResumed(); + }); + } + super.didChangeAppLifecycleState(state); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Text( + //現在時刻表示 + "Now : ${formatTime(DateTime.now())}", + ), + Text( + //タイマー表示 + formatTime(_ringingAlarm.displayTime), + style: Theme.of(context).textTheme.headline2, + ), + Row( + //各種ボタンの配置 + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FloatingActionButton( + //前の画面に戻る + heroTag: 'back_button', + onPressed: () { + if (_clockTimer != null) _clockTimer!.cancel(); + Navigator.of(context).pop(); + }, + child: const Text("Back"), + ), + FloatingActionButton( + //タイマーの一時停止 + heroTag: 'stop_button', + onPressed: () { + _ringingAlarm.cancelTimer(); + }, + child: const Text("Stop"), + ), + FloatingActionButton( + //タイマーのカウントダウン再開 + heroTag: 'start_button', + onPressed: () { + _ringingAlarm.startTimer(); + }, + child: const Text("Start"), + ), + FloatingActionButton( + //タイマーの時間をリセットしてスタート + heroTag: 'restart_button', + onPressed: () { + _ringingAlarm.restartTimer(); + }, + child: const Text("Restart"), + ), + FloatingActionButton( + //鳴ってるアラームを止める + heroTag: 'reset_button', + onPressed: () { + _ringingAlarm.resetNotification(); + }, + child: const Text("Reset"), + ), + ], + ) + ])); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..d2b4c55 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import flutter_local_notifications func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 4319405..4f887e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" async: dependency: transitive description: @@ -50,6 +57,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.5" + dbus: + dependency: transitive + description: + name: dbus + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5" fake_async: dependency: transitive description: @@ -57,6 +71,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" flutter: dependency: "direct main" description: flutter @@ -69,11 +97,39 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + url: "https://pub.dartlang.org" + source: hosted + version: "9.6.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" lints: dependency: transitive description: @@ -88,6 +144,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material: + dependency: "direct main" + description: + name: material + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0+2" material_color_utilities: dependency: transitive description: @@ -109,6 +172,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" sky_engine: dependency: transitive description: flutter @@ -156,6 +247,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.9" + timezone: + dependency: "direct main" + description: + name: timezone + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.0" vector_math: dependency: transitive description: @@ -163,5 +261,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" sdks: dart: ">=2.17.1 <3.0.0" + flutter: ">=2.2.0" diff --git a/pubspec.yaml b/pubspec.yaml index 232b61c..4a2e95d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,10 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 + material: ^1.0.0+2 + intl: ^0.17.0 + flutter_local_notifications: ^9.6.0 + timezone: ^0.8.0 dev_dependencies: flutter_test: