Skip to content

Commit

Permalink
Merge pull request #1163 from bannzai/revert/remove/code/migrate-loca…
Browse files Browse the repository at this point in the history
…l-notification

iOSはクイックレコードをネイティブで処理する
  • Loading branch information
bannzai committed May 9, 2024
2 parents 26c2962 + 904ce76 commit a86562d
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 2 deletions.
109 changes: 109 additions & 0 deletions ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ private var channel: FlutterMethodChannel?
}
}
}
configureNotificationActionableButtons()
UNUserNotificationCenter.current().swizzle()
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ["repeat_notification_for_taken_pill", "remind_notification_for_taken_pill"])
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ["repeat_notification_for_taken_pill", "remind_notification_for_taken_pill"])
FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in
Expand Down Expand Up @@ -219,11 +221,118 @@ private func analytics(name: String, parameters: [String: Any]? = nil, function:
channel?.invokeMethod("analytics", arguments: ["name": name, "parameters": parameters ?? [:]])
}

// MARK: - Avoid bug for flutter app badger
// ref: https://github.com/g123k/flutter_app_badger/pull/52
extension UNUserNotificationCenter {
func swizzle() {
guard let fromMethod = class_getInstanceMethod(type(of: self), #selector(UNUserNotificationCenter.setNotificationCategories(_:))) else {
fatalError()
}
guard let toMethod = class_getInstanceMethod(type(of: self), #selector(UNUserNotificationCenter.setNotificationCategories_methodSwizzle(_:))) else {
fatalError()
}

method_exchangeImplementations(fromMethod, toMethod)
}

@objc func setNotificationCategories_methodSwizzle(_ categories: Set<UNNotificationCategory>) {
if categories.isEmpty {
return
}
setNotificationCategories_methodSwizzle(categories)
}
}

// MARK: - Notification
extension AppDelegate {
func migrateFrom_1_3_2() {
if let salvagedValue = UserDefaults.standard.string(forKey: "startSavedDate"), let lastTakenDate = UserDefaults.standard.string(forKey: "savedDate") {
channel?.invokeMethod("salvagedOldStartTakenDate", arguments: ["salvagedOldStartTakenDate": salvagedValue, "salvagedOldLastTakenDate": lastTakenDate])
}
}

func configureNotificationActionableButtons() {
let recordAction = UNNotificationAction(identifier: "RECORD_PILL",
title: "飲んだ")
let category =
UNNotificationCategory(identifier: Category.pillReminder.rawValue,
actions: [recordAction],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: "",
options: .customDismissAction)
UNUserNotificationCenter.current().setNotificationCategories([category])
}

// NOTE: [LOCAL_NOTIFICATION] async/await版のメソッドは使わない。
// FlutterPluginAppLifeCycleDelegateから呼び出しているのがwithCompletionHandler付きのものなので合わせる
// https://chromium.googlesource.com/external/github.com/flutter/engine/+/refs/heads/flutter-2.5-candidate.8/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate.mm#283

// NOTE: このメソッドをoverrideすることでplugin側の処理は呼ばれないことに注意する。
// 常に一緒な結果をcompletionHandlerで実行すれば良いのでoverrideしても問題はない
override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
if #available(iOS 15.0, *) {
analytics(name: "will_present", parameters: ["notification_id" : notification.request.identifier, "content_title": notification.request.content.title, "content_body": notification.request.content.body, "content_interruptionLevel": notification.request.content.interruptionLevel.rawValue])
} else {
// Fallback on earlier versions
}
UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { requests in
analytics(name: "pending_notifications", parameters: ["length": requests.count])
})
if #available(iOS 14.0, *) {
completionHandler([.banner, .list, .sound, .badge])
} else {
completionHandler([.alert, .sound, .badge])
}
}

override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
func end() {
var isCompleted: Bool = false
let completionHandlerWrapper = {
isCompleted = true
completionHandler()
}

super.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandlerWrapper)

if !isCompleted {
completionHandlerWrapper()
}
}

switch extractCategory(userInfo: response.notification.request.content.userInfo) ?? Category(rawValue: response.notification.request.content.categoryIdentifier) {
case .pillReminder:
switch response.actionIdentifier {
case "RECORD_PILL":
// 先にバッジをクリアしてしまう。後述の理由でQuickRecordが多少遅延するため操作に違和感が出る。この部分は楽観的UIとして更新してしまう
UIApplication.shared.applicationIconBadgeNumber = 0

// application(_:didFinishLaunchingWithOptions:)が終了してからFlutterのmainの開始は非同期的でFlutterのmainの完了までラグがある
// 特にアプリのプロセスがKillされている状態では、先にuserNotificationCenter(_:didReceive:withCompletionHandler:)の処理が走り
// Flutter側でのMethodChannelが確立される前にQuickRecordの呼び出しをおこなってしまう。この場合次にChanelが確立するまでFlutter側の処理の実行は遅延される。これは次のアプリの起動時まで遅延されるとほぼ同義になる
// よって対処療法的ではあるが、5秒待つことでほぼ間違いなくmain(の中でもMethodChanelの確立までは)の処理はすべて終えているとしてここではdelayを設けている。
// ちなみに通常は1秒前後あれば十分であるが念のためくらいの間を持たせている
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [self] in
channel?.invokeMethod("recordPill", arguments: nil, result: { result in
end()
})
}
default:
end()
}
case nil:
return
}
}

enum Category: String {
case pillReminder = "PILL_REMINDER"
}

func extractCategory(userInfo: [AnyHashable: Any]) -> Category? {
guard let apns = userInfo["aps"] as? [String: Any], let category = apns["category"] as? String else {
return nil
}
return Category(rawValue: category)
}
}
1 change: 1 addition & 0 deletions lib/entrypoint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Future<void> entrypoint() async {
}, (error, stack) => FirebaseCrashlytics.instance.recordError(error, stack));
}

// iOSはmethodChannel経由の方が呼ばれる。iOSはネイティブの方のコードで上書きされる模様。現在はAndroidのために定義
@pragma('vm:entry-point')
Future<void> handleNotificationAction(NotificationResponse notificationResponse) async {
if (notificationResponse.actionId == actionIdentifier) {
Expand Down
1 change: 0 additions & 1 deletion lib/features/record/components/button/taken_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class TakenButton extends HookConsumerWidget {
required this.userIsPremiumOtTrial,
required this.registerReminderLocalNotification,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final takePill = ref.watch(takePillProvider);
Expand Down
55 changes: 55 additions & 0 deletions lib/native/channel.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,66 @@
import 'package:flutter/services.dart';
import 'package:pilll/utils/analytics.dart';
import 'package:pilll/native/legacy.dart';
import 'package:pilll/native/pill.dart';
import 'package:pilll/native/widget.dart';
import 'package:pilll/utils/error_log.dart';
import 'package:pilll/utils/local_notification.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:pilll/provider/database.dart';

const methodChannel = MethodChannel("method.channel.MizukiOhashi.Pilll");
void definedChannel() {
methodChannel.setMethodCallHandler((MethodCall call) async {
switch (call.method) {
case 'recordPill':
await LocalNotificationService.setupTimeZone();

// 通知からの起動の時に、FirebaseAuth.instanceを参照すると、まだinitializeされてないよ.的なエラーが出る
if (Firebase.apps.isEmpty) {
await Firebase.initializeApp();
}
final firebaseUser = FirebaseAuth.instance.currentUser;
if (firebaseUser == null) {
return;
}

try {
analytics.logEvent(name: "handle_recordPill_method_channel");

final database = DatabaseConnection(firebaseUser.uid);

final pillSheetGroup = await quickRecordTakePill(database);
syncActivePillSheetValue(pillSheetGroup: pillSheetGroup);

final cancelReminderLocalNotification = CancelReminderLocalNotification();
// エンティティの変更があった場合にdatabaseの読み込みで最新の状態を取得するために、Future.microtaskで更新を待ってから処理を始める
// hour,minute,番号を基準にIDを決定しているので、時間変更や番号変更時にそれまで登録されていたIDを特定するのが不可能なので全てキャンセルする
await (Future.microtask(() => null), cancelReminderLocalNotification()).wait;

final activePillSheet = pillSheetGroup?.activePillSheet;
final user = (await database.userReference().get()).data();
final setting = user?.setting;
if (pillSheetGroup != null && activePillSheet != null && user != null && setting != null) {
await RegisterReminderLocalNotification.run(
pillSheetGroup: pillSheetGroup,
activePillSheet: activePillSheet,
premiumOrTrial: user.isPremium || user.isTrial,
setting: setting,
);
}
} catch (e, st) {
errorLogger.recordError(e, st);

// errorLoggerに記録した後に実行する。これも失敗する可能性がある
await localNotificationService.plugin.show(
fallbackNotificationIdentifier,
"服用記録が失敗した可能性があります",
"アプリを開いてご確認ください",
null,
);
}
return;
case "salvagedOldStartTakenDate":
return salvagedOldStartTakenDate(call.arguments);
case "analytics":
Expand Down
3 changes: 2 additions & 1 deletion lib/utils/local_notification.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';

import 'dart:math';

Expand Down Expand Up @@ -76,7 +77,7 @@ class LocalNotificationService {
defaultPresentList: true,
),
),
onDidReceiveBackgroundNotificationResponse: handleNotificationAction,
onDidReceiveBackgroundNotificationResponse: Platform.isAndroid ? handleNotificationAction : null,
);
}

Expand Down

0 comments on commit a86562d

Please sign in to comment.