Skip to content

Commit

Permalink
Merge pull request #2 from OUCC/feature/#1-RingingAlarm
Browse files Browse the repository at this point in the history
Feature/#1-ringing_alarm

ありがとうございます。マージします。

- スリープのバグについて
  間違いなく他にもあると思いますが、おそらく改良は難しくないかと
- アプリ表示時の音
  アプリ表示時にも通知を送るなら簡単です。アプリ表示時は通知を送らず音を鳴らすなら単にアプリ内でmp3を再生する方法になると思います。この場合同時に画面切り替えも行いたいですね。
  • Loading branch information
MrMocchy authored Jun 10, 2022
2 parents 35904d1 + 705349e commit f69f16f
Show file tree
Hide file tree
Showing 9 changed files with 447 additions and 2 deletions.
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

0 comments on commit f69f16f

Please sign in to comment.