454 lines
16 KiB
Dart
454 lines
16 KiB
Dart
|
|
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<dynamic> _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<String?> fcmToken = BehaviorSubject.seeded(null);
|
||
|
|
|
||
|
|
Stream<String?> 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<String>? 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<String?> 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<AccountService>().refreshFcmToken();
|
||
|
|
});
|
||
|
|
fetchToken();
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<bool> 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<AuthorizationStatus> getNotificationAuthorizationStatus() async {
|
||
|
|
final notificationSettings =
|
||
|
|
await _firebaseMessaging.getNotificationSettings();
|
||
|
|
return notificationSettings.authorizationStatus;
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<bool> 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<bool> _requestNotificationPermissionForAndroid(
|
||
|
|
{String style = "default",
|
||
|
|
String scene = "",
|
||
|
|
Completer<RationaleResult> 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<bool> _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<bool> requestNotificationPermission(
|
||
|
|
{String style = "default",
|
||
|
|
String scene = "",
|
||
|
|
Completer<RationaleResult> 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();
|
||
|
|
}
|
||
|
|
}
|