Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#1-ringing_alarm #2

Merged
merged 2 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 18 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
Expand All @@ -30,5 +32,20 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service
android:name="com.dexterous.flutterlocalnotifications.ForegroundService"
android:exported="false"
android:stopWithTask="false"/>
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
</intent-filter>
</receiver>
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
</application>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!--<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>起動時に自動的に開始-->
<uses-permission android:name="android.permission.VIBRATE" />
</manifest>
Binary file not shown.
20 changes: 19 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
@@ -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());
}

Expand Down Expand Up @@ -102,10 +105,25 @@ class _MyHomePageState extends State<MyHomePage> {
'$_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),
Expand Down
161 changes: 161 additions & 0 deletions lib/model/ringing_alarm_model.dart
Original file line number Diff line number Diff line change
@@ -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<void> _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;
}
}
121 changes: 121 additions & 0 deletions lib/view/ringing_alarm_view.dart
Original file line number Diff line number Diff line change
@@ -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<RingingAlarmTestPage>
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"),
),
],
)
]));
}
}
2 changes: 2 additions & 0 deletions macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation

import flutter_local_notifications

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
}
Loading