diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 1ac400e27..7abf7d6dd 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -152,6 +152,7 @@ + diff --git a/src/Android/Receivers/NotificationDismissReceiver.cs b/src/Android/Receivers/NotificationDismissReceiver.cs new file mode 100644 index 000000000..43f69ea62 --- /dev/null +++ b/src/Android/Receivers/NotificationDismissReceiver.cs @@ -0,0 +1,41 @@ +using Android.Content; +using Bit.App.Abstractions; +using Bit.App.Models; +using Bit.App.Services; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using CoreConstants = Bit.Core.Constants; + +namespace Bit.Droid.Receivers +{ + [BroadcastReceiver(Name = Constants.PACKAGE_NAME + "." + nameof(NotificationDismissReceiver), Exported = false)] + public class NotificationDismissReceiver : BroadcastReceiver + { + private readonly LazyResolve _pushNotificationListenerService = new LazyResolve(); + private readonly LazyResolve _logger = new LazyResolve(); + + public override void OnReceive(Context context, Intent intent) + { + try + { + if (intent?.GetStringExtra(CoreConstants.NotificationData) is string notificationDataJson) + { + var notificationType = JToken.Parse(notificationDataJson).SelectToken(CoreConstants.NotificationDataType); + if (notificationType.ToString() == PasswordlessNotificationData.TYPE) + { + _pushNotificationListenerService.Value.OnNotificationDismissed(JsonConvert.DeserializeObject(notificationDataJson)).FireAndForget(); + } + } + } + catch (System.Exception ex) + { + _logger.Value.Exception(ex); + } + } + } +} + diff --git a/src/Android/Services/AndroidPushNotificationService.cs b/src/Android/Services/AndroidPushNotificationService.cs index 03f8ad672..6c5383f8f 100644 --- a/src/Android/Services/AndroidPushNotificationService.cs +++ b/src/Android/Services/AndroidPushNotificationService.cs @@ -10,9 +10,12 @@ using Bit.App.Abstractions; using Bit.App.Models; using Bit.Core; using Bit.Core.Abstractions; +using Bit.Droid.Receivers; using Bit.Droid.Utilities; using Newtonsoft.Json; using Xamarin.Forms; +using static Xamarin.Essentials.Platform; +using Intent = Android.Content.Intent; namespace Bit.Droid.Services { @@ -79,16 +82,21 @@ namespace Bit.Droid.Services var context = Android.App.Application.Context; var intent = new Intent(context, typeof(MainActivity)); - intent.PutExtra(Core.Constants.NotificationData, JsonConvert.SerializeObject(data)); - + intent.PutExtra(Bit.Core.Constants.NotificationData, JsonConvert.SerializeObject(data)); var pendingIntentFlags = AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true); var pendingIntent = PendingIntent.GetActivity(context, 20220801, intent, pendingIntentFlags); - var builder = new NotificationCompat.Builder(context, Core.Constants.AndroidNotificationChannelId) + + var deleteIntent = new Intent(context, typeof(NotificationDismissReceiver)); + deleteIntent.PutExtra(Bit.Core.Constants.NotificationData, JsonConvert.SerializeObject(data)); + var deletePendingIntent = PendingIntent.GetBroadcast(context, 20220802, deleteIntent, pendingIntentFlags); + + var builder = new NotificationCompat.Builder(context, Bit.Core.Constants.AndroidNotificationChannelId) .SetContentIntent(pendingIntent) .SetContentTitle(title) .SetContentText(message) .SetSmallIcon(Resource.Drawable.ic_notification) .SetColor((int)Android.Graphics.Color.White) + .SetDeleteIntent(deletePendingIntent) .SetAutoCancel(true); if (data is PasswordlessNotificationData passwordlessNotificationData && passwordlessNotificationData.TimeoutInMinutes > 0) diff --git a/src/App/Abstractions/IPushNotificationListenerService.cs b/src/App/Abstractions/IPushNotificationListenerService.cs index f5d2cb39f..fdbb6ca88 100644 --- a/src/App/Abstractions/IPushNotificationListenerService.cs +++ b/src/App/Abstractions/IPushNotificationListenerService.cs @@ -11,6 +11,7 @@ namespace Bit.App.Abstractions void OnUnregistered(string device); void OnError(string message, string device); Task OnNotificationTapped(BaseNotificationData data); + Task OnNotificationDismissed(BaseNotificationData data); bool ShouldShowNotification(); } } diff --git a/src/App/Services/NoopPushNotificationListenerService.cs b/src/App/Services/NoopPushNotificationListenerService.cs index b8b6b2890..c70c43479 100644 --- a/src/App/Services/NoopPushNotificationListenerService.cs +++ b/src/App/Services/NoopPushNotificationListenerService.cs @@ -34,5 +34,10 @@ namespace Bit.App.Services { return Task.FromResult(0); } + + public Task OnNotificationDismissed(BaseNotificationData data) + { + return Task.FromResult(0); + } } } diff --git a/src/App/Services/PushNotificationListenerService.cs b/src/App/Services/PushNotificationListenerService.cs index b2a58f627..5b5f38b90 100644 --- a/src/App/Services/PushNotificationListenerService.cs +++ b/src/App/Services/PushNotificationListenerService.cs @@ -26,20 +26,18 @@ namespace Bit.App.Services const string TAG = "##PUSH NOTIFICATIONS"; private bool _showNotification; - private bool _resolved; - private ISyncService _syncService; - private IStateService _stateService; - private IAppIdService _appIdService; - private IApiService _apiService; - private IMessagingService _messagingService; - private IPushNotificationService _pushNotificationService; - private ILogger _logger; + private LazyResolve _syncService = new LazyResolve(); + private LazyResolve _stateService = new LazyResolve(); + private LazyResolve _appIdService = new LazyResolve(); + private LazyResolve _apiService = new LazyResolve(); + private LazyResolve _messagingService = new LazyResolve(); + private LazyResolve _pushNotificationService = new LazyResolve(); + private LazyResolve _logger = new LazyResolve(); public async Task OnMessageAsync(JObject value, string deviceType) { Debug.WriteLine($"{TAG} OnMessageAsync called"); - Resolve(); if (value == null) { return; @@ -65,14 +63,14 @@ namespace Bit.App.Services Debug.WriteLine($"{TAG} - Notification object created: t:{notification?.Type} - p:{notification?.Payload}"); - var appId = await _appIdService.GetAppIdAsync(); + var appId = await _appIdService.Value.GetAppIdAsync(); if (notification?.Payload == null || notification.ContextId == appId) { return; } - var myUserId = await _stateService.GetActiveUserIdAsync(); - var isAuthenticated = await _stateService.IsAuthenticatedAsync(); + var myUserId = await _stateService.Value.GetActiveUserIdAsync(); + var isAuthenticated = await _stateService.Value.IsAuthenticatedAsync(); switch (notification.Type) { case NotificationType.SyncCipherUpdate: @@ -81,7 +79,7 @@ namespace Bit.App.Services notification.Payload); if (isAuthenticated && cipherCreateUpdateMessage.UserId == myUserId) { - await _syncService.SyncUpsertCipherAsync(cipherCreateUpdateMessage, + await _syncService.Value.SyncUpsertCipherAsync(cipherCreateUpdateMessage, notification.Type == NotificationType.SyncCipherUpdate); } break; @@ -91,7 +89,7 @@ namespace Bit.App.Services notification.Payload); if (isAuthenticated && folderCreateUpdateMessage.UserId == myUserId) { - await _syncService.SyncUpsertFolderAsync(folderCreateUpdateMessage, + await _syncService.Value.SyncUpsertFolderAsync(folderCreateUpdateMessage, notification.Type == NotificationType.SyncFolderUpdate); } break; @@ -101,7 +99,7 @@ namespace Bit.App.Services notification.Payload); if (isAuthenticated && loginDeleteMessage.UserId == myUserId) { - await _syncService.SyncDeleteCipherAsync(loginDeleteMessage); + await _syncService.Value.SyncDeleteCipherAsync(loginDeleteMessage); } break; case NotificationType.SyncFolderDelete: @@ -109,7 +107,7 @@ namespace Bit.App.Services notification.Payload); if (isAuthenticated && folderDeleteMessage.UserId == myUserId) { - await _syncService.SyncDeleteFolderAsync(folderDeleteMessage); + await _syncService.Value.SyncDeleteFolderAsync(folderDeleteMessage); } break; case NotificationType.SyncCiphers: @@ -117,27 +115,27 @@ namespace Bit.App.Services case NotificationType.SyncSettings: if (isAuthenticated) { - await _syncService.FullSyncAsync(false); + await _syncService.Value.FullSyncAsync(false); } break; case NotificationType.SyncOrgKeys: if (isAuthenticated) { - await _apiService.RefreshIdentityTokenAsync(); - await _syncService.FullSyncAsync(true); + await _apiService.Value.RefreshIdentityTokenAsync(); + await _syncService.Value.FullSyncAsync(true); } break; case NotificationType.LogOut: if (isAuthenticated) { - _messagingService.Send("logout"); + _messagingService.Value.Send("logout"); } break; case NotificationType.AuthRequest: var passwordlessLoginMessage = JsonConvert.DeserializeObject(notification.Payload); // if the user has not enabled passwordless logins ignore requests - if (!await _stateService.GetApprovePasswordlessLoginsAsync(passwordlessLoginMessage?.UserId)) + if (!await _stateService.Value.GetApprovePasswordlessLoginsAsync(passwordlessLoginMessage?.UserId)) { return; } @@ -148,8 +146,8 @@ namespace Bit.App.Services return; } - await _stateService.SetPasswordlessLoginNotificationAsync(passwordlessLoginMessage, passwordlessLoginMessage?.UserId); - var userEmail = await _stateService.GetEmailAsync(passwordlessLoginMessage?.UserId); + await _stateService.Value.SetPasswordlessLoginNotificationAsync(passwordlessLoginMessage, passwordlessLoginMessage?.UserId); + var userEmail = await _stateService.Value.GetEmailAsync(passwordlessLoginMessage?.UserId); var notificationData = new PasswordlessNotificationData() { @@ -158,8 +156,8 @@ namespace Bit.App.Services UserEmail = userEmail, }; - _pushNotificationService.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), notificationData); - _messagingService.Send("passwordlessLoginRequest", passwordlessLoginMessage); + _pushNotificationService.Value.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), notificationData); + _messagingService.Value.Send("passwordlessLoginRequest", passwordlessLoginMessage); break; default: break; @@ -168,31 +166,30 @@ namespace Bit.App.Services public async Task OnRegisteredAsync(string token, string deviceType) { - Resolve(); Debug.WriteLine($"{TAG} - Device Registered - Token : {token}"); - var isAuthenticated = await _stateService.IsAuthenticatedAsync(); + var isAuthenticated = await _stateService.Value.IsAuthenticatedAsync(); if (!isAuthenticated) { Debug.WriteLine($"{TAG} - not auth"); return; } - var appId = await _appIdService.GetAppIdAsync(); + var appId = await _appIdService.Value.GetAppIdAsync(); try { #if DEBUG - await _stateService.SetPushInstallationRegistrationErrorAsync(null); + await _stateService.Value.SetPushInstallationRegistrationErrorAsync(null); #endif - await _apiService.PutDeviceTokenAsync(appId, + await _apiService.Value.PutDeviceTokenAsync(appId, new Core.Models.Request.DeviceTokenRequest { PushToken = token }); Debug.WriteLine($"{TAG} Registered device with server."); - await _stateService.SetPushLastRegistrationDateAsync(DateTime.UtcNow); + await _stateService.Value.SetPushLastRegistrationDateAsync(DateTime.UtcNow); if (deviceType == Device.Android) { - await _stateService.SetPushCurrentTokenAsync(token); + await _stateService.Value.SetPushCurrentTokenAsync(token); } } #if DEBUG @@ -200,11 +197,11 @@ namespace Bit.App.Services { Debug.WriteLine($"{TAG} Failed to register device."); - await _stateService.SetPushInstallationRegistrationErrorAsync(apiEx.Error?.Message); + await _stateService.Value.SetPushInstallationRegistrationErrorAsync(apiEx.Error?.Message); } catch (Exception e) { - await _stateService.SetPushInstallationRegistrationErrorAsync(e.Message); + await _stateService.Value.SetPushInstallationRegistrationErrorAsync(e.Message); throw; } #else @@ -226,22 +223,44 @@ namespace Bit.App.Services public async Task OnNotificationTapped(BaseNotificationData data) { - Resolve(); try { if (data is PasswordlessNotificationData passwordlessNotificationData) { - var notificationUserId = await _stateService.GetUserIdAsync(passwordlessNotificationData.UserEmail); + var notificationUserId = await _stateService.Value.GetUserIdAsync(passwordlessNotificationData.UserEmail); if (notificationUserId != null) { - await _stateService.SetActiveUserAsync(notificationUserId); - _messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT); + await _stateService.Value.SetActiveUserAsync(notificationUserId); + _messagingService.Value.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT); } } } catch (Exception ex) { - _logger.Exception(ex); + _logger.Value.Exception(ex); + } + } + + public async Task OnNotificationDismissed(BaseNotificationData data) + { + try + { + if (data is PasswordlessNotificationData passwordlessNotificationData) + { + var notificationUserId = await _stateService.Value.GetUserIdAsync(passwordlessNotificationData.UserEmail); + if (notificationUserId != null) + { + var savedNotification = await _stateService.Value.GetPasswordlessLoginNotificationAsync(notificationUserId); + if (savedNotification != null) + { + await _stateService.Value.SetPasswordlessLoginNotificationAsync(null, notificationUserId); + } + } + } + } + catch (Exception ex) + { + _logger.Value.Exception(ex); } } @@ -249,22 +268,6 @@ namespace Bit.App.Services { return _showNotification; } - - private void Resolve() - { - if (_resolved) - { - return; - } - _syncService = ServiceContainer.Resolve("syncService"); - _stateService = ServiceContainer.Resolve("stateService"); - _appIdService = ServiceContainer.Resolve("appIdService"); - _apiService = ServiceContainer.Resolve("apiService"); - _messagingService = ServiceContainer.Resolve("messagingService"); - _pushNotificationService = ServiceContainer.Resolve(); - _logger = ServiceContainer.Resolve(); - _resolved = true; - } } } #endif diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 7250c02b8..903ef5e7e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -33,7 +33,7 @@ public const string PasswordlessNotificationId = "26072022"; public const string AndroidNotificationChannelId = "general_notification_channel"; public const string NotificationData = "notificationData"; - public const string NotificationDataType = "NotificationType"; + public const string NotificationDataType = "Type"; public const int SelectFileRequestCode = 42; public const int SelectFilePermissionRequestCode = 43; public const int SaveFileRequestCode = 44; diff --git a/src/iOS/Services/iOSPushNotificationHandler.cs b/src/iOS/Services/iOSPushNotificationHandler.cs index de484d5d2..b19fe17e0 100644 --- a/src/iOS/Services/iOSPushNotificationHandler.cs +++ b/src/iOS/Services/iOSPushNotificationHandler.cs @@ -96,23 +96,35 @@ namespace Bit.iOS.Services public void DidReceiveNotificationResponse(UNUserNotificationCenter center, UNNotificationResponse response, Action completionHandler) { Debug.WriteLine($"{TAG} DidReceiveNotificationResponse {response?.Notification?.Request?.Content?.UserInfo}"); - - if (response.IsDefaultAction && response?.Notification?.Request?.Content?.UserInfo != null) + if ((response?.Notification?.Request?.Content?.UserInfo) == null) { - var userInfo = response?.Notification?.Request?.Content?.UserInfo; - OnMessageReceived(userInfo); + completionHandler(); + return; + } - if (userInfo.TryGetValue(NSString.FromObject(Constants.NotificationData), out NSObject nsObject)) + var userInfo = response?.Notification?.Request?.Content?.UserInfo; + OnMessageReceived(userInfo); + + if (userInfo.TryGetValue(NSString.FromObject(Constants.NotificationData), out NSObject nsObject)) + { + var token = JToken.Parse(NSString.FromObject(nsObject).ToString()); + var typeToken = token.SelectToken(Constants.NotificationDataType); + if (response.IsDefaultAction) { - var token = JToken.Parse(NSString.FromObject(nsObject).ToString()); - var typeToken = token.SelectToken(Constants.NotificationDataType); if (typeToken.ToString() == PasswordlessNotificationData.TYPE) { _pushNotificationListenerService.OnNotificationTapped(token.ToObject()); } } + else if (response.IsDismissAction) + { + if (typeToken.ToString() == PasswordlessNotificationData.TYPE) + { + _pushNotificationListenerService.OnNotificationDismissed(token.ToObject()); + } + } } - + // Inform caller it has been handled completionHandler(); }