TF-2877 Create notification data source & repo

This commit is contained in:
DatDang
2024-06-04 10:02:19 +07:00
committed by Dat H. Pham
parent 2652a99e0e
commit b89820618c
16 changed files with 550 additions and 2 deletions
@@ -1,11 +1,17 @@
package com.linagora.android.tmail
import android.app.NotificationManager
import android.content.Context
import android.os.Bundle
import android.os.PersistableBundle
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.os.Build
import android.text.TextUtils
import androidx.core.app.NotificationManagerCompat
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterFragmentActivity() {
@@ -20,5 +26,7 @@ class MainActivity: FlutterFragmentActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
NotificationGroup().register(flutterEngine, applicationContext)
}
}
@@ -0,0 +1,48 @@
package com.linagora.android.tmail
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.text.TextUtils
import androidx.core.app.NotificationManagerCompat
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class NotificationGroup {
private val channel = "com.linagora.android.teammail.notification.group.permission"
fun register(flutterEngine: FlutterEngine, applicationContext: Context) {
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel).setMethodCallHandler { call, result ->
try {
if (call.method == "getNotificationGroupPermission") {
val groupId = call.arguments
if (groupId is String) {
val isEnabled = getNotificationChannelGroupStatus(applicationContext, groupId)
result.success(isEnabled)
} else {
result.error("Unavailable", "No such group", "")
}
} else {
result.notImplemented()
}
} catch (exception: Throwable) {
result.error("UnknownError", exception.message, "")
}
}
}
private fun getNotificationChannelGroupStatus(context: Context, groupId: String?): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (!TextUtils.isEmpty(groupId)) {
val manager = context.getSystemService(FlutterFragmentActivity.NOTIFICATION_SERVICE) as NotificationManager
val group = manager.getNotificationChannelGroup(groupId)
return !group.isBlocked
}
return false
} else {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}
}
+1 -1
View File
@@ -71,7 +71,7 @@ post_install do |installer|
'PERMISSION_LOCATION=0',
## dart: PermissionGroup.notification
'PERMISSION_NOTIFICATIONS=0',
'PERMISSION_NOTIFICATIONS=1',
## dart: PermissionGroup.mediaLibrary
'PERMISSION_MEDIA_LIBRARY=0',
+7 -1
View File
@@ -1,4 +1,6 @@
PODS:
- app_settings (5.1.1):
- Flutter
- AppAuth (1.7.4):
- AppAuth/Core (= 1.7.4)
- AppAuth/ExternalUserAgent (= 1.7.4)
@@ -185,6 +187,7 @@ PODS:
- Flutter
DEPENDENCIES:
- app_settings (from `.symlinks/plugins/app_settings/ios`)
- better_open_file (from `.symlinks/plugins/better_open_file/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- contacts_service (from `.symlinks/plugins/contacts_service/ios`)
@@ -237,6 +240,8 @@ SPEC REPOS:
- SwiftyGif
EXTERNAL SOURCES:
app_settings:
:path: ".symlinks/plugins/app_settings/ios"
better_open_file:
:path: ".symlinks/plugins/better_open_file/ios"
connectivity_plus:
@@ -295,6 +300,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc
AppAuth: 182c5b88630569df5acb672720534756c29b3358
better_open_file: 03cf320415d4d3f46b6e00adc4a567d76c1a399d
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
@@ -343,6 +349,6 @@ SPEC CHECKSUMS:
url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: 91d9116a6c482e0d0b230a68097981fae7e4d07a
PODFILE CHECKSUM: ded4724b53568389542fa0f7d7a64c05ffc03971
COCOAPODS: 1.14.3
@@ -0,0 +1,7 @@
import 'package:jmap_dart_client/jmap/core/user_name.dart';
abstract class NotificationDataSource {
Future<bool> getNotificationSetting(UserName userName);
Future<void> toggleNotificationSetting();
}
@@ -0,0 +1,42 @@
import 'package:app_settings/app_settings.dart';
import 'package:core/utils/platform_info.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:tmail_ui_user/features/manage_account/data/datasource/notification_datasource.dart';
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
import 'package:tmail_ui_user/main/permissions/notification_permission_service.dart';
class NotificationDataSourceImpl implements NotificationDataSource {
final NotificationPermissionService _permissionService;
final ExceptionThrower _exceptionThrower;
NotificationDataSourceImpl(
this._permissionService,
this._exceptionThrower);
@override
Future<bool> getNotificationSetting(UserName userName) {
return Future.sync(() async {
if (PlatformInfo.isIOS) {
return await _checkIosNotificationPermissionEnabled();
} else {
return await _checkAndroidNotificationPermissionEnabled(userName);
}
}).catchError(_exceptionThrower.throwException);
}
@override
Future<void> toggleNotificationSetting() async {
return Future.sync(() async {
return await AppSettings.openAppSettings(type: AppSettingsType.notification);
}).catchError(_exceptionThrower.throwException);
}
Future<bool> _checkIosNotificationPermissionEnabled() {
return _permissionService.isGranted(Permission.notification);
}
Future<bool> _checkAndroidNotificationPermissionEnabled(UserName userName) {
return _permissionService.checkNotificationPermissionEnabled(userName.value);
}
}
@@ -0,0 +1,19 @@
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:tmail_ui_user/features/manage_account/data/datasource/notification_datasource.dart';
import 'package:tmail_ui_user/features/manage_account/domain/repository/notification_repository.dart';
class NotificationRepositoryImpl implements NotificationRepository {
final NotificationDataSource _notificationDataSource;
NotificationRepositoryImpl(this._notificationDataSource);
@override
Future<bool> getNotificationSetting(UserName userName) {
return _notificationDataSource.getNotificationSetting(userName);
}
@override
Future<void> toggleNotificationSetting() {
return _notificationDataSource.toggleNotificationSetting();
}
}
@@ -0,0 +1,7 @@
import 'package:jmap_dart_client/jmap/core/user_name.dart';
abstract class NotificationRepository {
Future<bool> getNotificationSetting(UserName userName);
Future<void> toggleNotificationSetting();
}
@@ -0,0 +1,28 @@
import 'package:permission_handler/permission_handler.dart';
import 'package:tmail_ui_user/features/base/isolate/background_isolate_binary_messenger/background_isolate_binary_messenger_mobile.dart';
import 'package:tmail_ui_user/main/permissions/permission_service.dart';
class NotificationPermissionService extends PermissionService {
Future<bool> checkGroupPermissionByGroupId(String groupId) async {
if (groupId.isEmpty) {
return false;
}
const methodChannel = MethodChannel('com.linagora.android.teammail.notification.group.permission');
const methodName = 'getNotificationGroupPermission';
final invokeResult = await methodChannel.invokeMethod<bool>(
methodName,
groupId);
return invokeResult ?? false;
}
Future<bool> checkNotificationPermissionEnabled(String groupId) async {
final permissionGranted = await isGranted(Permission.notification);
if (!permissionGranted) {
return false;
}
return await checkGroupPermissionByGroupId(groupId);
}
}
@@ -0,0 +1,17 @@
import 'package:permission_handler/permission_handler.dart';
class PermissionService {
Future<bool> isGranted(Permission permission) async {
final current = await permission.status;
switch (current) {
case PermissionStatus.granted:
return true;
case PermissionStatus.denied:
final requestResult = await permission.request();
final permissionGranted = requestResult == PermissionStatus.granted;
return permissionGranted;
default:
return false;
}
}
}
+8
View File
@@ -25,6 +25,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.13.0"
app_settings:
dependency: "direct main"
description:
name: app_settings
sha256: "09bc7fe0313a507087bec1a3baf555f0576e816a760cbb31813a88890a09d9e5"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
archive:
dependency: transitive
description:
+2
View File
@@ -244,6 +244,8 @@ dependencies:
file_picker: 5.3.1
app_settings: 5.1.1
dev_dependencies:
flutter_test:
sdk: flutter
@@ -0,0 +1,101 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:tmail_ui_user/features/manage_account/data/datasource_impl/notification_datasource_impl.dart';
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
import 'package:tmail_ui_user/main/permissions/notification_permission_service.dart';
import 'notification_datasource_impl_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<NotificationPermissionService>(),
MockSpec<ExceptionThrower>()])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final permissionService = MockNotificationPermissionService();
final exceptionThrower = MockExceptionThrower();
final notificationDatasourceImpl = NotificationDataSourceImpl(
permissionService,
exceptionThrower);
final userName = UserName('value');
void setMockNotificationPermission({
VoidCallback? openAppSettingsCallback,
}) {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.spencerccf.app_settings/methods'),
(message) async {
if (message.method == 'openSettings') {
openAppSettingsCallback?.call();
return true;
}
return null;
});
}
group('notification datasource test:', () {
group('getAppNotificationSetting test:', () {
test(
'should return true '
'when user grants notification permission',
() async {
// arrange
debugDefaultTargetPlatformOverride = TargetPlatform.android;
when(permissionService.checkNotificationPermissionEnabled(any)).thenAnswer((_) async => true);
// act
final result = await notificationDatasourceImpl.getNotificationSetting(userName);
// assert
verify(permissionService.checkNotificationPermissionEnabled(userName.value));
expect(result, true);
debugDefaultTargetPlatformOverride = null;
});
test(
'should return false '
'when user denies notification permission',
() async {
// arrange
debugDefaultTargetPlatformOverride = TargetPlatform.android;
when(permissionService.checkNotificationPermissionEnabled(any)).thenAnswer((_) async => false);
// act
final result = await notificationDatasourceImpl.getNotificationSetting(userName);
// assert
verify(permissionService.checkNotificationPermissionEnabled(userName.value));
expect(result, false);
debugDefaultTargetPlatformOverride = null;
});
});
group('toggleAppNotificationSetting test:', () {
test(
'should call openAppSettings '
'when user toggles notification setting on',
() async {
// arrange
debugDefaultTargetPlatformOverride = TargetPlatform.android;
bool openAppSettingsCalled = false;
setMockNotificationPermission(openAppSettingsCallback: () {
openAppSettingsCalled = true;
});
// act
await notificationDatasourceImpl.toggleNotificationSetting();
// assert
expect(openAppSettingsCalled, true);
debugDefaultTargetPlatformOverride = null;
});
});
});
}
@@ -0,0 +1,48 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:tmail_ui_user/features/manage_account/data/datasource/notification_datasource.dart';
import 'package:tmail_ui_user/features/manage_account/data/repository/notification_repository_impl.dart';
import 'notification_repository_impl_test.mocks.dart';
@GenerateNiceMocks([MockSpec<NotificationDataSource>()])
void main() {
final notificationDataSource = MockNotificationDataSource();
final notificationRepositoryImpl = NotificationRepositoryImpl(notificationDataSource);
final userName = UserName('value');
group('notification repository impl test:', () {
test(
'should call getNotificationSetting '
'when getNotificationSetting in datasource is called',
() {
// arrange
when(notificationDataSource.getNotificationSetting(any))
.thenAnswer((_) async => true);
// act
notificationRepositoryImpl.getNotificationSetting(userName);
// assert
verify(notificationDataSource.getNotificationSetting(userName)).called(1);
});
test(
'should call toggleNotificationSetting '
'when toggleNotificationSetting in datasource is called',
() {
// arrange
when(notificationDataSource.toggleNotificationSetting())
.thenAnswer((_) async => true);
// act
notificationRepositoryImpl.toggleNotificationSetting();
// assert
verify(notificationDataSource.toggleNotificationSetting()).called(1);
});
});
}
@@ -0,0 +1,123 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:tmail_ui_user/features/base/isolate/background_isolate_binary_messenger/background_isolate_binary_messenger_mobile.dart';
import 'package:tmail_ui_user/main/permissions/notification_permission_service.dart';
class TestNotificationPermissionService extends NotificationPermissionService {
bool callCheckGroupPermission = false;
@override
Future<bool> checkGroupPermissionByGroupId(String groupId) {
callCheckGroupPermission = true;
return super.checkGroupPermissionByGroupId(groupId);
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final notificationPermissionService = TestNotificationPermissionService();
void setMockNotificationPermission({
required PermissionStatus currentStatus,
PermissionStatus? requestedStatus,
}) {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('flutter.baseflow.com/permissions/methods'),
(message) async {
if (message.method == 'checkPermissionStatus') {
return currentStatus.index;
} else if (message.method == 'requestPermissions') {
assert(
requestedStatus != null,
'mock requested status must not be null '
'when requesting permission');
return {Permission.notification.value: requestedStatus!.index};
}
return null;
});
}
void setMockNotificationGroupStatus({
required bool groupIsBlocked,
}) {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.linagora.android.teammail.notification.group.permission'),
(message) async {
if (message.method == 'getNotificationGroupPermission') {
return groupIsBlocked;
}
return null;
});
}
group('notification permission service test', () {
test(
'should return false '
'when checkGroupPermission is called '
'and group id is empty',
() async {
// arrange
setMockNotificationGroupStatus(groupIsBlocked: true);
// act
final result = await notificationPermissionService.checkGroupPermissionByGroupId('');
// assert
expect(notificationPermissionService.callCheckGroupPermission, true);
expect(result, false);
});
test(
'should return the status of notification group '
'when checkGroupPermission is called '
'and group id is not empty',
() async {
// arrange
setMockNotificationGroupStatus(groupIsBlocked: true);
// act
final result = await notificationPermissionService.checkGroupPermissionByGroupId('test');
// assert
expect(notificationPermissionService.callCheckGroupPermission, true);
expect(result, true);
});
test(
'should return false '
'when checkNotificationPermissionEnabled is called '
'and permission is not granted',
() async {
// arrange
setMockNotificationPermission(currentStatus: PermissionStatus.permanentlyDenied);
// act
final result = await notificationPermissionService.checkNotificationPermissionEnabled('');
// assert
expect(result, false);
});
test(
'should return the status of notification group '
'when checkNotificationPermissionEnabled is called '
'and permission is not granted',
() async {
// arrange
setMockNotificationPermission(currentStatus: PermissionStatus.granted);
setMockNotificationGroupStatus(groupIsBlocked: true);
// act
final result = await notificationPermissionService.checkGroupPermissionByGroupId('test');
// assert
expect(result, true);
});
});
}
@@ -0,0 +1,84 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:tmail_ui_user/main/permissions/permission_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final permissionService = PermissionService();
void setMockNotificationPermission({
required PermissionStatus currentStatus,
PermissionStatus? requestedStatus,
}) {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('flutter.baseflow.com/permissions/methods'),
(message) async {
if (message.method == 'checkPermissionStatus') {
return currentStatus.index;
} else if (message.method == 'requestPermissions') {
assert(
requestedStatus != null,
'mock requested status must not be null '
'when requesting permission');
return {Permission.notification.value: requestedStatus!.index};
}
return null;
});
}
group('permission service test:', () {
group('isGranted test:', () {
test(
'should return true '
'when permission is granted before',
() async {
// arrange
setMockNotificationPermission(currentStatus: PermissionStatus.granted);
// act
final result = await permissionService.isGranted(Permission.notification);
// assert
expect(result, true);
});
test(
'should return true '
'when permission is not granted before '
'and granted when request',
() async {
// arrange
setMockNotificationPermission(
currentStatus: PermissionStatus.denied,
requestedStatus: PermissionStatus.granted);
// act
final result = await permissionService.isGranted(Permission.notification);
// assert
expect(result, true);
});
test(
'should return false '
'when permission is not granted before '
'and denied when request',
() async {
// arrange
setMockNotificationPermission(
currentStatus: PermissionStatus.denied,
requestedStatus: PermissionStatus.denied);
// act
final result = await permissionService.isGranted(Permission.notification);
// assert
expect(result, false);
});
});
});
}