diff --git a/src/App/App.csproj b/src/App/App.csproj index 2a4e3bcae..5e0da5cbe 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -2,9 +2,13 @@ net8.0-android;net8.0-ios - $(TargetFrameworks);net8.0-windows10.0.19041.0 - - + + + + + + + Exe Bit.App true @@ -35,6 +39,17 @@ x64 --> + + True + False + True + + + True + False + False + False + false iossimulator-arm64 diff --git a/src/App/MauiProgram.cs b/src/App/MauiProgram.cs index b1ed570c7..708884c73 100644 --- a/src/App/MauiProgram.cs +++ b/src/App/MauiProgram.cs @@ -29,6 +29,8 @@ namespace Bit.App Bit.App.Handlers.StepperHandlerMappings.Setup(); Bit.App.Handlers.TimePickerHandlerMappings.Setup(); Bit.App.Handlers.ButtonHandlerMappings.Setup(); + + handlers.AddHandler(typeof(TabbedPage), typeof(Bit.App.Handlers.CustomTabbedPageHandler)); #else iOS.Core.Handlers.ButtonHandlerMappings.Setup(); iOS.Core.Handlers.DatePickerHandlerMappings.Setup(); diff --git a/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs b/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs new file mode 100644 index 000000000..ddd72b638 --- /dev/null +++ b/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs @@ -0,0 +1,121 @@ +using AndroidX.AppCompat.View.Menu; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Google.Android.Material.BottomNavigation; +using Microsoft.Maui.Handlers; + +namespace Bit.App.Handlers +{ + public partial class CustomTabbedPageHandler : TabbedViewHandler + { + private TabbedPage _tabbedPage; + private BottomNavigationView _bottomNavigationView; + private Android.Views.ViewGroup _bottomNavigationViewGroup; + private ILogger _logger; + + protected override void ConnectHandler(global::Android.Views.View platformView) + { + _logger = ServiceContainer.Resolve("logger"); + + if(VirtualView is TabbedPage tabbedPage) + { + _tabbedPage = tabbedPage; + _tabbedPage.Loaded += TabbedPage_Loaded; + } + + base.ConnectHandler(platformView); + } + + private void TabbedPage_Loaded(object sender, EventArgs e) + { + try + { + //This layout should always be the same/fixed and therefore this should run with no issues. Nevertheless it's wrapped in try catch to avoid crashing in edge-case scenarios. + _bottomNavigationViewGroup = (((sender as VisualElement).Handler as IPlatformViewHandler) + .PlatformView + .Parent + .Parent as Android.Views.View) + .FindViewById(Microsoft.Maui.Controls.Resource.Id.navigationlayout_bottomtabs) as Android.Views.ViewGroup; + } + catch (Exception ex) + { + _logger.Exception(ex); + } + + if(_bottomNavigationViewGroup == null) { return; } + + //If TabbedPage still doesn't have items we set an event to wait for them + if (_bottomNavigationViewGroup.ChildCount == 0) + { + _bottomNavigationViewGroup.ChildViewAdded += View_ChildViewAdded; + } + else + { //If we already have items we can start listening immediately + var bottomTabs = _bottomNavigationViewGroup.GetChildAt(0); + ListenToItemReselected(bottomTabs); + } + } + + private void ListenToItemReselected(Android.Views.View bottomTabs) + { + if(bottomTabs is BottomNavigationView bottomNavigationView) + { + //If there was an older _bottomNavigationView for some reason we want to make sure to unregister + if(_bottomNavigationView != null) + { + _bottomNavigationView.ItemReselected -= BottomNavigationView_ItemReselected; + _bottomNavigationView = null; + } + + _bottomNavigationView = bottomNavigationView; + _bottomNavigationView.ItemReselected += BottomNavigationView_ItemReselected; + } + } + + private void View_ChildViewAdded(object sender, Android.Views.ViewGroup.ChildViewAddedEventArgs e) + { + //We shouldn't need this to be called anymore times so we can unregister to the events now + if(_bottomNavigationViewGroup != null) + { + _bottomNavigationViewGroup.ChildViewAdded -= View_ChildViewAdded; + } + + var bottomTabs = e.Child; + ListenToItemReselected(bottomTabs); + } + + private void BottomNavigationView_ItemReselected(object sender, Google.Android.Material.Navigation.NavigationBarView.ItemReselectedEventArgs e) + { + if(e.Item is MenuItemImpl item) + { + System.Diagnostics.Debug.WriteLine($"Tab '{item.Title}' was reselected so we'll PopToRoot."); + MainThread.BeginInvokeOnMainThread(async () => await _tabbedPage.CurrentPage.Navigation.PopToRootAsync()); + } + } + + protected override void DisconnectHandler(global::Android.Views.View platformView) + { + if(_bottomNavigationViewGroup != null) + { + _bottomNavigationViewGroup.ChildViewAdded -= View_ChildViewAdded; + _bottomNavigationViewGroup = null; + } + + if(_bottomNavigationView != null) + { + _bottomNavigationView.ItemReselected -= BottomNavigationView_ItemReselected; + _bottomNavigationView = null; + } + + if(_tabbedPage != null) + { + _tabbedPage.Loaded -= TabbedPage_Loaded; + _tabbedPage = null; + } + + _logger = null; + + base.DisconnectHandler(platformView); + } + } +} diff --git a/src/Core/Pages/Generator/GeneratorPageViewModel.cs b/src/Core/Pages/Generator/GeneratorPageViewModel.cs index 03f5fcbae..9600d32ff 100644 --- a/src/Core/Pages/Generator/GeneratorPageViewModel.cs +++ b/src/Core/Pages/Generator/GeneratorPageViewModel.cs @@ -338,7 +338,10 @@ namespace Bit.App.Pages public string PlusAddressedEmail { - get => _usernameOptions.PlusAddressedEmail; + get + { + return _usernameOptions?.PlusAddressedEmail ?? string.Empty; + } set { if (_usernameOptions != null && _usernameOptions.PlusAddressedEmail != value) @@ -850,7 +853,7 @@ namespace Bit.App.Pages } } - await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, message, AppResources.Ok)); + await MainThread.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, message, AppResources.Ok)); } private string GetUsernameTypeLabelDescription(UsernameType value) diff --git a/src/Core/Pages/PickerViewModel.cs b/src/Core/Pages/PickerViewModel.cs index 4050208b8..43168ecdc 100644 --- a/src/Core/Pages/PickerViewModel.cs +++ b/src/Core/Pages/PickerViewModel.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; +using Bit.App.Abstractions; using Bit.Core.Resources.Localization; using Bit.Core.Abstractions; using Bit.Core.Utilities; -using Microsoft.Maui.ApplicationModel; using Bit.App.Utilities; namespace Bit.App.Pages @@ -49,6 +44,8 @@ namespace Bit.App.Pages { get { + if (_items == null) { return string.Empty; } + if (_items.TryGetValue(_selectedKey, out var option)) { return option; diff --git a/src/Core/Pages/TabsPage.cs b/src/Core/Pages/TabsPage.cs index 23bedb178..f14917a70 100644 --- a/src/Core/Pages/TabsPage.cs +++ b/src/Core/Pages/TabsPage.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading.Tasks; -using Bit.App.Effects; +using Bit.App.Effects; using Bit.App.Models; using Bit.Core.Resources.Localization; using Bit.App.Utilities; @@ -8,8 +6,6 @@ using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Models.Data; using Bit.Core.Utilities; -using Microsoft.Maui.Controls; -using Microsoft.Maui; namespace Bit.App.Pages { @@ -60,8 +56,7 @@ namespace Bit.App.Pages }; Children.Add(settingsPage); - // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes - if (Device.RuntimePlatform == Device.Android) + if (DeviceInfo.Platform == DevicePlatform.Android) { Effects.Add(new TabBarEffect()); @@ -93,7 +88,7 @@ namespace Bit.App.Pages { if (message.Command == "syncCompleted") { - Device.BeginInvokeOnMainThread(async () => await UpdateVaultButtonTitleAsync()); + MainThread.BeginInvokeOnMainThread(async () => await UpdateVaultButtonTitleAsync()); } }); await UpdateVaultButtonTitleAsync(); @@ -131,7 +126,7 @@ namespace Bit.App.Pages CurrentPage = _sendGroupingsPage; } - protected async override void OnCurrentPageChanged() + protected override async void OnCurrentPageChanged() { if (CurrentPage is NavigationPage navPage) {