import 'dart:async'; import 'dart:io'; import 'dart:ui'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/services.dart'; import 'package:guru_app/account/account_manager.dart'; import 'package:guru_app/analytics/guru_analytics.dart'; import 'package:guru_app/guru_app.dart'; import 'package:guru_app/lifecycle/lifecycle_model.dart'; import 'package:guru_app/property/app_property.dart'; import 'package:guru_app/property/property_keys.dart'; import 'package:guru_utils/controller/lifecycle_controller.dart'; import 'package:guru_utils/lifecycle/lifecycle_manager.dart'; import 'package:guru_utils/router/router.dart'; import 'package:guru_utils/extensions/extensions.dart'; import 'package:guru_utils/log/log.dart'; import 'package:guru_utils/math/math_utils.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:json_annotation/json_annotation.dart'; /// Created by Haoyi on 5/14/21 // Future _backgroundMessageHandler(RemoteMessage message) async { Log.d("_backgroundMessageHandler:${message.data} ${message.data["uri"]}"); return; } enum RationaleResult { skip, allow } enum PromptTrigger { @JsonValue(0) rationale, // 依赖Android原生的shouldShowRequestRationale返回值来展示对应的Rationale页面 @JsonValue(1) request // 依赖请求的次数来展示对应的Rationale页面 } class RemoteMessagingManager { static RemoteMessagingManager instance = RemoteMessagingManager._(); late FirebaseMessaging _firebaseMessaging; final BehaviorSubject fcmToken = BehaviorSubject.seeded(null); Stream get observableFCMToken => fcmToken.stream; RemoteMessagingManager._(); int _retryFetchTokenCount = 0; final _statusMap = { AuthorizationStatus.authorized: "granted", AuthorizationStatus.denied: "denied", AuthorizationStatus.provisional: "provisional", AuthorizationStatus.notDetermined: "not_determined" }; String? _getUriLastSegment(String? uri) { try { return Uri.parse(uri ?? "").pathSegments.last; } catch (error) { return null; } } void fetchToken({Completer? completer}) async { Log.d("Fetch FCMToken!!"); String? token; try { token = await _firebaseMessaging.getToken(); if (token != null) { fcmToken.addEx(token); // RuntimeProperty.instance.setString("firebase_push_token", token); Log.d("### FCMToken :$token"); } } catch (error, stacktrace) { Log.d("fetchToken error!", error: error, stackTrace: stacktrace); } if (token == null || token == '') { final intervalSeconds = (MathUtils.fibonacci(_retryFetchTokenCount) * 8).clamp(8, 600); Future.delayed(Duration(seconds: intervalSeconds), () { fetchToken(); }); _retryFetchTokenCount++; } else { _retryFetchTokenCount = 0; completer?.complete(Future.value(token)); } } Future getToken() async { final result = fcmToken.value ?? (await _firebaseMessaging.getToken()); if (result != null && fcmToken.value == null) { fcmToken.addEx(result); } return result; } void init() async { _firebaseMessaging = FirebaseMessaging.instance; final granted = await checkNotificationPermission(); if (!granted) { Future.delayed(const Duration(seconds: 8), () async { if (GuruApp .instance.appSpec.deployment.autoRequestNotificationPermission) { Log.d("guru_app auto request notification permissions!"); requestNotificationPermission(); } else { Log.d("guru_app check notification permissions!"); final shouldShowRequestRationale = await Permission.notification.shouldShowRequestRationale; Log.d( "guru_app post request notification permission event! shouldShowRequestRationale:$shouldShowRequestRationale"); LifecycleManager.instance.postEvent( RequestNotificationPermissionEvent( rationale: shouldShowRequestRationale)); } }); } FirebaseMessaging.instance.getInitialMessage().then((message) { if (message != null) { final uri = message.data["uri"]; if (uri != null && uri is String && uri.isNotEmpty) { Log.d("getInitialMessage:${message.data} $uri"); RouteCenter.instance.dispatchUri(Uri.parse(uri)); } } }); FirebaseMessaging.onMessage.listen((message) { Log.d("onMessage:${message.data}"); final data = message.data; // final notification = message.notification; // if (Platform.isAndroid && notification != null) { // NotificationChannel.showNotification( // NotificationChannel.pushType, {"title": notification.title, "body": notification.body, "cmd": data["cmd"], "uri": data["uri"]}); // } GuruAnalytics.instance.logEventEx("push_receive", itemCategory: data["cmd"], itemName: _getUriLastSegment(data["uri"])); }); FirebaseMessaging.onMessageOpenedApp.listen((message) { final uri = message.data["uri"]; if (uri != null && uri is String && uri.isNotEmpty) { Log.d("onMessageOpenApp:${message.data} ${message.data["uri"]}"); RouteCenter.instance.dispatchUri(Uri.parse(message.data["uri"])); } }); _firebaseMessaging.onTokenRefresh.listen((event) { Log.d("onTokenRefresh $event"); AccountManager.instance.refreshFcmToken(); // Injector.provide().refreshFcmToken(); }); fetchToken(); } Future checkNotificationPermission() async { final _map = { AuthorizationStatus.authorized: "granted", AuthorizationStatus.denied: "denied", AuthorizationStatus.provisional: "provisional", AuthorizationStatus.notDetermined: "not_determined" }; final notificationSettings = await _firebaseMessaging.getNotificationSettings(); final property = _map[notificationSettings.authorizationStatus]; if (property != null) { GuruAnalytics.instance.setUserProperty("noti_perm", property); } else { GuruAnalytics.instance.setUserProperty("noti_perm", "not_determined"); } return notificationSettings.authorizationStatus == AuthorizationStatus.authorized; } Future getNotificationAuthorizationStatus() async { final notificationSettings = await _firebaseMessaging.getNotificationSettings(); return notificationSettings.authorizationStatus; } Future isShouldShowRequestRationale() async { if (GuruApp .instance.appSpec.deployment.notificationPermissionPromptTrigger == PromptTrigger.rationale) { return await Permission.notification.shouldShowRequestRationale; } if (await Permission.notification.isGranted) { return false; } final permanentlyDenied = await Permission.notification.isPermanentlyDenied; if (permanentlyDenied) { return false; } int deniedTimes = await AppProperty.getInstance() .getInt(PropertyKeys.deniedNotificationPermissionTimes, defValue: 0); if (deniedTimes >= 2) { return false; } final requestTimes = await AppProperty.getInstance() .getInt(PropertyKeys.requestNotificationPermissionTimes, defValue: 0); return requestTimes >= 1; } Future _requestNotificationPermissionForAndroid( {String style = "default", String scene = "", Completer Function()? showRationale}) async { final PromptTrigger promptTrigger = GuruApp.instance.appSpec.deployment.notificationPermissionPromptTrigger; if (await Permission.notification.isGranted) { GuruAnalytics.instance.setUserProperty("noti_perm", "granted"); return true; } else { final permanentlyDenied = await Permission.notification.isPermanentlyDenied; if (permanentlyDenied) { GuruAnalytics.instance.setUserProperty("noti_perm", "denied"); return false; } int deniedTimes = await AppProperty.getInstance() .getInt(PropertyKeys.deniedNotificationPermissionTimes, defValue: 0); if (deniedTimes >= 2) { GuruAnalytics.instance.setUserProperty("noti_perm", "denied"); return false; } final promptTriggerValue = promptTrigger == PromptTrigger.rationale ? "a" : "b"; final requestTimes = await AppProperty.getInstance().increaseAndGet( PropertyKeys.requestNotificationPermissionTimes, defValue: 0); final trackingNotificationPermissionPass = GuruApp .instance.appSpec.deployment.trackingNotificationPermissionPass && requestTimes < (GuruApp.instance.appSpec.deployment .trackingNotificationPermissionPassLimitTimes); if (trackingNotificationPermissionPass) { GuruAnalytics.instance.logEventEx("noti_perm_req_$requestTimes", itemCategory: style, itemName: scene, parameters: { "request_times": requestTimes, "denied_times": deniedTimes, "prompt_trigger": promptTriggerValue }); } final shouldShowRequestRationale = await Permission.notification.shouldShowRequestRationale || (promptTrigger == PromptTrigger.request && requestTimes > 1); Log.d( "_requestNotificationPermission requestTimes:$requestTimes deniedTimes:$deniedTimes trackingNotificationPermissionPass:$trackingNotificationPermissionPass promptTrigger:$promptTrigger shouldShowRequestRationale:$shouldShowRequestRationale "); if (shouldShowRequestRationale && showRationale != null) { GuruAnalytics.instance.logEventEx("noti_perm_rationale_imp", itemCategory: style, itemName: scene); RationaleResult rationaleResult = RationaleResult.skip; try { final completer = showRationale(); rationaleResult = await completer.future; } catch (error, stacktrace) { Log.d("showRationale error!", error: error, stackTrace: stacktrace); } GuruAnalytics.instance.logEventEx("noti_perm_rationale_result", itemCategory: style, itemName: scene, parameters: { "result": rationaleResult == RationaleResult.allow ? "allow" : "skip", }); if (rationaleResult == RationaleResult.skip) { return false; } } final showTimes = await AppProperty.getInstance().increaseAndGet( PropertyKeys.showNotificationPermissionTimes, defValue: 0); GuruAnalytics.instance.logEventEx("noti_perm_imp", itemCategory: style, itemName: scene, parameters: { "show_times": showTimes, "request_times": requestTimes, "denied_times": deniedTimes, "prompt_trigger": promptTriggerValue }); final requestSettings = await _firebaseMessaging.requestPermission(); final result = _statusMap[requestSettings.authorizationStatus] ?? "not_determined"; await GuruAnalytics.instance.setUserProperty("noti_perm", result); if (requestSettings.authorizationStatus != AuthorizationStatus.authorized) { final shouldShowRequestRationale2 = await Permission.notification.shouldShowRequestRationale; if (deniedTimes == 0 && shouldShowRequestRationale2) { deniedTimes = 1; await AppProperty.getInstance() .setInt(PropertyKeys.deniedNotificationPermissionTimes, 1); } else if (deniedTimes == 1 && shouldShowRequestRationale != shouldShowRequestRationale2) { deniedTimes = 2; await AppProperty.getInstance() .setInt(PropertyKeys.deniedNotificationPermissionTimes, 2); } } else { if (trackingNotificationPermissionPass) { GuruAnalytics.instance.logEventEx("noti_perm_pass_$requestTimes", itemCategory: style, itemName: scene, parameters: { "show_times": showTimes, "request_times": requestTimes, "denied_times": deniedTimes, "prompt_trigger": promptTriggerValue }); } } GuruAnalytics.instance.logEventEx("noti_perm_result", itemCategory: style, itemName: scene, parameters: { "result": result, "show_times": showTimes, "request_times": requestTimes, "denied_times": deniedTimes, "prompt_trigger": promptTriggerValue }); Log.d( "notificationSettings.authorizationStatus:${requestSettings.authorizationStatus} showTimes:$requestTimes deniedTimes:$deniedTimes promptTrigger: $promptTrigger"); return requestSettings.authorizationStatus == AuthorizationStatus.authorized; } } Future _requestNotificationPermissionForIOS( {String style = "default", String scene = ""}) async { final status = await getNotificationAuthorizationStatus(); switch (status) { case AuthorizationStatus.authorized: GuruAnalytics.instance.setUserProperty("noti_perm", "granted"); return true; case AuthorizationStatus.provisional: GuruAnalytics.instance.setUserProperty("noti_perm", "provisional"); return true; case AuthorizationStatus.denied: GuruAnalytics.instance.setUserProperty("noti_perm", "denied"); return false; default: break; } final trackingNotificationPermissionPass = GuruApp.instance.appSpec.deployment.trackingNotificationPermissionPass; int deniedTimes = await AppProperty.getInstance() .getInt(PropertyKeys.deniedNotificationPermissionTimes, defValue: 0); final requestTimes = await AppProperty.getInstance().increaseAndGet( PropertyKeys.requestNotificationPermissionTimes, defValue: 0); final showTimes = await AppProperty.getInstance().increaseAndGet( PropertyKeys.showNotificationPermissionTimes, defValue: 0); if (trackingNotificationPermissionPass) { GuruAnalytics.instance.logEventEx("noti_perm_req_$requestTimes", itemCategory: style, itemName: scene, parameters: { "request_times": requestTimes, "denied_times": deniedTimes, "prompt_trigger": "a" }); } final requestSettings = await _firebaseMessaging.requestPermission(); final result = _statusMap[requestSettings.authorizationStatus] ?? "not_determined"; await GuruAnalytics.instance.setUserProperty("noti_perm", result); if (requestSettings.authorizationStatus != AuthorizationStatus.authorized) { deniedTimes += 1; } else { if (trackingNotificationPermissionPass) { GuruAnalytics.instance.logEventEx("noti_perm_pass_$requestTimes", itemCategory: style, itemName: scene, parameters: { "show_times": showTimes, "request_times": requestTimes, "denied_times": deniedTimes, "prompt_trigger": "a" }); } } GuruAnalytics.instance.logEventEx("noti_perm_result", itemCategory: style, itemName: scene, parameters: { "result": result, "show_times": showTimes, "request_times": requestTimes, "denied_times": deniedTimes, "prompt_trigger": "a" }); Log.d( "notificationSettings.authorizationStatus:${requestSettings.authorizationStatus} showTimes:$requestTimes deniedTimes:$deniedTimes"); return requestSettings.authorizationStatus == AuthorizationStatus.authorized; } Future requestNotificationPermission( {String style = "default", String scene = "", Completer Function()? showRationale}) async { if (Platform.isAndroid) { return _requestNotificationPermissionForAndroid( style: style, scene: scene, showRationale: showRationale); } else if (Platform.isIOS) { return _requestNotificationPermissionForIOS(style: style, scene: scene); } return false; } void saveTokenToClipboard() { final token = fcmToken.value; if (token != null) { Clipboard.setData(ClipboardData(text: token)); Log.d("saveTokenToClipboard:$token"); } } void dispose() { fcmToken.close(); } }