Proof of concept for having multiple window in Android for autofill support and navigating with the help of an Extended splash page.

This commit is contained in:
Dinis Vieira 2023-12-13 23:42:58 +00:00
parent 5cbef47fd4
commit 05858bea48
No known key found for this signature in database
GPG key ID: 9389160FF6C295F3
5 changed files with 146 additions and 14 deletions

View file

@ -10,6 +10,7 @@ using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Response;
using Bit.Core.Pages;
using Bit.Core.Services;
using Bit.Core.Utilities;
@ -74,6 +75,8 @@ namespace Bit.App
}
public static Window CurrentWindow { get; private set; }
public static Window AutofillWindow { get; private set; }
public static Window MainWindow { get; private set; }
public void SetOptions(AppOptions appOptions)
{
@ -84,28 +87,28 @@ namespace Bit.App
{
if (activationState != null
&& activationState.State.TryGetValue("autofillFramework", out string autofillFramework)
&& autofillFramework == "true") //TODO: There are likely better ways to filter this. Maybe using Options.
&& autofillFramework == "true"
&& activationState.State.ContainsKey("autofillFrameworkCipherId")) //AutofillExternalActivity
{
if (activationState.State.ContainsKey("autofillFrameworkCipherId")) //TODO: There are likely better ways to filter this. Maybe using Options.
{
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
}
else
{
//TODO: IMPORTANT Question: this works if we want to show the CipherSelection, but we are skipping all our ASYNC Navigation workflows where we (for example) check if the user is logged in and/or needs to enter auth again.
var autofillWindow = new Window(new NavigationPage(new CipherSelectionPage(Options)));
return autofillWindow;
}
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
}
else if (Options.FromAutofillFramework || Options.Uri != null || Options.OtpData != null || Options.CreateSend != null) //"Internal" Autofill and Uri/Otp/CreateSend
{
AutofillWindow = new Window(new NavigationPage(new AndroidExtSplashPage(Options)));
CurrentWindow = AutofillWindow;
return CurrentWindow;
}
else if(CurrentWindow != null)
{
//TODO: This likely crashes if we try to have two apps side-by-side on Android
//TODO: Question: In these scenarios should a new Window be created or can the same one be reused?
CurrentWindow = MainWindow;
return CurrentWindow;
}
else
{
CurrentWindow = new Window(new NavigationPage(new HomePage(Options)));
MainWindow = new Window(new NavigationPage(new HomePage(Options)));
CurrentWindow = MainWindow;
return CurrentWindow;
}
}

View file

@ -64,7 +64,6 @@
<Folder Include="Resources\Fonts\" />
<Folder Include="Effects\" />
<Folder Include="Resources\Raw\" />
<Folder Include="Pages\" />
<Folder Include="Behaviors\" />
<Folder Include="Controls\" />
<Folder Include="Lists\" />
@ -92,4 +91,9 @@
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<MauiXaml Update="Pages\AndroidExtSplashPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
</ItemGroup>
</Project>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.Core.Pages.AndroidExtSplashPage"
Title="AndroidExtSplashPage"
Loaded="AndroidExtSplashPage_OnLoaded">
<Grid>
<ActivityIndicator VerticalOptions="Center" HorizontalOptions="Center" IsRunning="True" />
</Grid>
</ContentPage>

View file

@ -0,0 +1,105 @@
using Bit.App.Models;
using Bit.App.Pages;
using Bit.App.Utilities.AccountManagement;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
namespace Bit.Core.Pages;
public partial class AndroidExtSplashPage : ContentPage
{
private IConditionedAwaiterManager _conditionedAwaiterManager;
private IVaultTimeoutService _vaultTimeoutService;
private IStateService _stateService;
private AppOptions _appOptions;
public AndroidExtSplashPage(AppOptions appOptions)
{
InitializeComponent();
_appOptions = appOptions;
_conditionedAwaiterManager = ServiceContainer.Resolve<IConditionedAwaiterManager>();
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_stateService = ServiceContainer.Resolve<IStateService>();
}
private async Task GetNavigationToExecuteAsync(AppOptions options, Window window)
{
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
var authed = await _stateService.IsAuthenticatedAsync();
if (authed)
{
if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() ||
await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
{
// TODO implement orgIdentifier flow to SSO Login page, same as email flow below
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
var email = await _stateService.GetEmailAsync();
options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null;
var navParams = new LoginNavigationParams(email);
if (navParams is LoginNavigationParams loginParams)
{
window.Page = new NavigationPage(new LoginPage(loginParams.Email, options));
}
}
else if (await _vaultTimeoutService.IsLockedAsync() ||
await _vaultTimeoutService.ShouldLockAsync())
{
/* //TODO: is lockParams needed here?
if (navParams is LockNavigationParams lockParams)
{
return () => new Window(new NavigationPage(new LockPage(Options, lockParams.AutoPromptBiometric)));
}
else
{*/
window.Page = new NavigationPage(new LockPage(options));
/*}*/
}
else if (options.FromAutofillFramework && options.SaveType.HasValue)
{
window.Page = new NavigationPage(new CipherAddEditPage(appOptions: options));
}
else if (options.Uri != null)
{
window.Page = new NavigationPage(new CipherSelectionPage(options));
}
else if (options.OtpData != null)
{
window.Page = new NavigationPage(new CipherSelectionPage(options));
}
else if (options.CreateSend != null)
{
window.Page = new NavigationPage(new SendAddEditPage(options));
}
else
{
window.Page = new TabsPage(options);
}
}
else
{
options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null;
if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() ||
await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
{
// TODO implement orgIdentifier flow to SSO Login page, same as email flow below
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
var email = await _stateService.GetEmailAsync();
await _stateService.SetRememberedEmailAsync(email);
window.Page = new NavigationPage(new HomePage(options));
}
else
{
window.Page = new NavigationPage(new HomePage(options));
}
}
}
private async void AndroidExtSplashPage_OnLoaded(object sender, EventArgs e)
{
await GetNavigationToExecuteAsync(_appOptions, this.Window);
}
}

View file

@ -144,7 +144,17 @@ namespace Bit.App.Pages
private void OnUnloaded(object sender, EventArgs e)
{
Handler?.DisconnectHandler();
try
{
Handler?.DisconnectHandler();
}
catch (Exception ex)
{
//Workaround: Currently the Disconnect Handler needs to be manually called from the App: https://github.com/dotnet/maui/issues/3604
// In some specific edges cases the MauiContext can be gone when we call this. (for example filling a field using Accessibility)
// In those scenarios the app should just be "closing" anyway, so we just want to avoid the exception.
System.Diagnostics.Debug.WriteLine(ex.Message);
}
}
public void ResetToVaultPage()