发生这种情况是因为您的IdentityService.LoginAsync()
任务实际上仍在后台等待自定义选项卡活动回调发生,无论自定义选项卡浏览器不再可见。由于用户在完成登录往返之前关闭,因此在用户未来尝试完成往返之前,不会触发回调。每次登录尝试都会创建一个新的等待任务,因此每次用户过早关闭自定义选项卡窗口时,等待任务的集合都会增加。
当用户实际完成登录往返时,很明显所有任务仍在等待,因为当等待已久的回调最终发生时,它们全部立即解冻。这提出了另一个需要处理的问题,因为除了最后一个任务之外的所有任务都会导致'invalid state'
oidc 错误结果。
我通过在开始新的登录尝试之前取消上一个任务来解决此问题。我添加了一个TryCancel
方法ChromeCustomTabsBrowser
在自定义界面上IBrowserExtra
。在里面ChromeCustomTabsBrowser.InvokeAsync
实施,保留参考TaskCompletionSource
被退回。
下次用户单击登录按钮时,TryCancel
之前首先被调用ChromeCustomTabsBrowser.LoginAsync
使用保留的参考来解锁仍在等待的先前登录尝试。
为了使这项工作顺利进行,IsBusy=True
应推迟到自定义选项卡回调之后(自定义选项卡浏览器无论如何都会位于顶部),以在单击自定义选项卡关闭按钮时保持 GUI 交互。否则用户将永远无法重新尝试登录。
Update:根据要求添加了示例代码。
public interface IBrowserExtra
{
void TryCancel();
}
public class ChromeCustomTabsBrowser : IBrowser, IBrowserExtra, IBrowserFallback
{
private readonly Activity _context;
private readonly CustomTabsActivityManager _manager;
private TaskCompletionSource<BrowserResult> _task;
private Action<string> _callback;
public ChromeCustomTabsBrowser()
{
_context = CrossCurrentActivity.Current.Activity;
_manager = new CustomTabsActivityManager(_context);
}
public Task<BrowserResult> InvokeAsync(BrowserOptions options)
{
var builder = new CustomTabsIntent.Builder(_manager.Session)
.SetToolbarColor(Color.Argb(255, 0, 0, 0))
.SetShowTitle(false)
.EnableUrlBarHiding()
.SetStartAnimations(_context, Android.Resource.Animation.SlideInLeft, Android.Resource.Animation.SlideOutRight)
.SetExitAnimations(_context, Android.Resource.Animation.SlideInLeft, Android.Resource.Animation.SlideOutRight);
var customTabsIntent = builder.Build();
// ensures the intent is not kept in the history stack, which makes
// sure navigating away from it will close it
customTabsIntent.Intent.AddFlags(ActivityFlags.NoHistory);
_callback = null;
_callback = url =>
{
UnsubscribeFromCallback();
_task.TrySetResult(new BrowserResult()
{
Response = url
});
};
SubscribeToCallback();
// Keep track of this task to be able to refer it from TryCancel later
_task = new TaskCompletionSource<BrowserResult>();
customTabsIntent.LaunchUrl(_context, Android.Net.Uri.Parse(options.StartUrl));
return _task.Task;
}
private void SubscribeToCallback()
{
OidcCallbackActivity.Callbacks += _callback;
}
private void UnsubscribeFromCallback()
{
OidcCallbackActivity.Callbacks -= _callback;
_callback = null;
}
void IBrowserExtra.TryCancel()
{
if (_callback != null)
{
UnsubscribeFromCallback();
}
if (_task != null)
{
_task.TrySetCanceled();
_task = null;
}
}
}
public class LoginService
{
private static OidcClient s_loginClient;
private Task<LoginResult> _loginChallengeTask;
private readonly IBrowser _browser;
private readonly IAppInfo _appInfo;
public LoginService(
IBrowser secureBrowser,
IBrowserFallback fallbackBrowser,
IAppInfo appInfo)
{
_appInfo = appInfo;
_browser = ChooseBrowser(appInfo, secureBrowser, fallbackBrowser);
}
private IBrowser ChooseBrowser(IAppInfo appInfo, IBrowser secureBrowser, IBrowserFallback fallbackBrowser)
{
return appInfo.PlatformSupportsSecureBrowserSession ? secureBrowser : fallbackBrowser as IBrowser;
}
public async Task<bool> StartLoginChallenge()
{
// Cancel any pending browser invocation task
EnsureNoLoginChallengeActive();
s_loginClient = OpenIdConnect.CreateOidcClient(_browser, _appInfo);
try
{
_loginChallengeTask = s_loginClient.LoginAsync(new LoginRequest()
{
FrontChannelExtraParameters = OpenIdConnectConfiguration.LoginExtraParams
});
// This triggers the custom tabs browser login session
var oidcResult = await _loginChallengeTask;
if (_loginChallengeTask.IsCanceled)
{
// task can be cancelled if a second login attempt was completed while the first
// was cancelled prematurely even before the browser view appeared.
return false;
}
else
{
// at this point we returned from the browser login session
if (oidcResult?.IsError ?? throw new LoginException("oidcResult is null."))
{
if (oidcResult.Error == "UserCancel")
{
// Graceful exit: user canceled using the close button on the browser view.
return false;
}
else
{
throw new LoginException(oidcResult.Error);
}
}
}
// we get here if browser session just popped and navigation is back at customer page
PerformPostLoginOperations(oidcResult);
return true;
}
catch (TaskCanceledException)
{
// swallow cancel exception.
// this can occur when user canceled browser session and restarted.
// Previous session is forcefully canceled at start of ExecuteLoginChallenge cauing this exception.
LogHelper.Debug($"'Login attempt was via browser roundtrip canceled.");
return false;
}
}
private void EnsureNoLoginChallengeActive()
{
if (IsLoginSessionStarted)
{
(_browser as IBrowserExtra)?.TryCancel();
}
}
private static bool IsLoginSessionStarted => s_loginClient != null;
}