using System; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; using OwlCore.AbstractStorage; using OwlCore.AbstractUI.Models; using OwlCore.Extensions; using OwlCore.Services; using StrixMusic.Cores.Files; using StrixMusic.Cores.Files.Models; using StrixMusic.Cores.OneDrive.ConfigPanels; using StrixMusic.Cores.OneDrive.Services; using StrixMusic.Cores.OneDrive.Storage; using StrixMusic.Sdk.AppModels; using StrixMusic.Sdk.CoreModels; using StrixMusic.Sdk.FileMetadata; using StrixMusic.Sdk.Services; namespace StrixMusic.Cores.OneDrive { /// /// Scan and play audio files from OneDrive. /// public sealed class OneDriveCore : FilesCore { private readonly IFolderData _metadataStorage; private readonly AbstractButton _completeGenericSetupButton; private AbstractUICollection _configPanel; /// /// Initializes a new instance of the class. /// /// A unique identifier for this core instance. /// A folder abstraction where this core can persist settings data beyond the lifetime of the application. /// A folder abstraction where this core can persist metadata for scanned files. /// A service that can notify the user with interactive UI or messages. public OneDriveCore(string instanceId, IFolderData settingsStorage, IFolderData metadataStorage, INotificationService notificationService) : base(instanceId) { _metadataStorage = metadataStorage; NotificationService = notificationService; Settings = new OneDriveCoreSettings(settingsStorage); _configPanel = new AbstractUICollection(string.Empty); _completeGenericSetupButton = new AbstractButton(Guid.NewGuid().ToString(), "OK"); _completeGenericSetupButton.Clicked += CompleteGenericSetupButton_Clicked; } /// /// Initializes a new instance of the class. /// /// /// This overload allows passing preconfigured settings that, if all values are valid, will allow initialization to complete without /// any interaction from the user. /// /// A unique identifier for this core instance. /// A preconfigured instance of that will be used instead of a new instance with default values. /// A folder abstraction where this core can persist metadata for scanned files. /// A service that can notify the user with interactive UI or messages. public OneDriveCore(string instanceId, OneDriveCoreSettings settings, IFolderData metadataStorage, INotificationService notificationService) : base(instanceId) { _metadataStorage = metadataStorage; NotificationService = notificationService; Settings = settings; _configPanel = new AbstractUICollection(string.Empty); _completeGenericSetupButton = new AbstractButton(Guid.NewGuid().ToString(), "OK"); _completeGenericSetupButton.Clicked += CompleteGenericSetupButton_Clicked; } /// public override CoreMetadata Registration { get; } = Metadata; /// /// The metadata that identifies this core before instantiation. /// public static CoreMetadata Metadata { get; } = new CoreMetadata(id: nameof(OneDriveCore), displayName: "OneDrive", logoUri: new Uri("ms-appx:///Assets/Cores/OneDrive/Logo.svg"), sdkVer: Version.Parse("0.0.0.0")); /// public override string InstanceDescriptor { get; set; } = string.Empty; /// public override AbstractUICollection AbstractConfigPanel => _configPanel; /// /// The message handler to use or requests (wherever possible). /// public HttpMessageHandler HttpMessageHandler { get; set; } = new HttpClientHandler(); /// /// The settings for this core instance. /// public OneDriveCoreSettings Settings { get; } /// /// The method that should be used for login. /// public LoginMethod LoginMethod { get; set; } /// /// Gets a service that can notify the user with interactive UI or generic messages. /// internal INotificationService NotificationService { get; } /// /// Raised when the user requests to visit an external web page for OneDrive login. /// public event EventHandler? LoginNavigationRequested; /// public event EventHandler? MsalPublicClientApplicationBuilderCreated; /// public event EventHandler? MsalAcquireTokenInteractiveParameterBuilderCreated; /// public override event EventHandler? CoreStateChanged; /// public override event EventHandler? InstanceDescriptorChanged; /// public override event EventHandler? AbstractConfigPanelChanged; /// public async override Task InitAsync(CancellationToken cancellationToken = default) { var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cancellationToken = cancellationTokenSource.Token; ChangeCoreState(CoreState.Loading); Logger.LogInformation("Getting setting values"); await Settings.LoadAsync(); using var onCancelledRegistration = cancellationTokenSource.Token.Register(() => ChangeCoreState(CoreState.Unloaded)); if (string.IsNullOrWhiteSpace(Settings.ClientId) || string.IsNullOrWhiteSpace(Settings.TenantId) || string.IsNullOrWhiteSpace(Settings.RedirectUri) || !Settings.UserHasSeenAuthClientKeysSettings) { Logger.LogInformation("Need custom app key values"); ChangeCoreState(CoreState.NeedsConfiguration); var oobePanel = new OutOfBoxExperiencePanel(Settings, NotificationService, cancellationToken); _configPanel = oobePanel; AbstractConfigPanelChanged?.Invoke(this, EventArgs.Empty); await oobePanel.ExecuteCustomAppKeyStageAsync(); Settings.UserHasSeenAuthClientKeysSettings = true; await Settings.SaveAsync(); } if (!Settings.UserHasSeenGeneralOobeSettings) { Logger.LogInformation("Displaying general OOBE settings"); ChangeCoreState(CoreState.NeedsConfiguration); var oobePanel = new OutOfBoxExperiencePanel(Settings, NotificationService, cancellationToken); _configPanel = oobePanel; AbstractConfigPanelChanged?.Invoke(this, EventArgs.Empty); await oobePanel.ExecuteSettingsStageAsync(); Settings.UserHasSeenGeneralOobeSettings = true; await Settings.SaveAsync(); } if (string.IsNullOrWhiteSpace(Settings.AccountIdentifier)) { Logger.LogInformation("User needs to login"); ChangeCoreState(CoreState.NeedsConfiguration); try { Settings.AccountIdentifier = await AcquireLoginAsync(cancellationToken); await Settings.SaveAsync(); } catch (HttpRequestException) { RaiseFailedConnectionState(); return; } catch (AggregateException ex) { if (ex.InnerExceptions.Any(x => x is HttpRequestException)) { ex.Handle(x => { if (x is not HttpRequestException) return false; RaiseFailedConnectionState(); return true; }); cancellationTokenSource.Cancel(); return; } } } if (string.IsNullOrWhiteSpace(Settings.SelectedFolderId)) { Logger.LogInformation("User needs to pick folder"); ChangeCoreState(CoreState.NeedsConfiguration); var folder = await AcquireUserSelectedFolderAsync(cancellationToken); if (folder is null) { cancellationTokenSource.Cancel(); return; } Settings.SelectedFolderId = folder.Id ?? string.Empty; await Settings.SaveAsync(); } Logger.LogInformation("Fully configured, setting state."); ChangeCoreState(CoreState.Configured); ChangeCoreState(CoreState.Loading); var authManager = new AuthenticationManager(Settings.ClientId, Settings.TenantId, Settings.RedirectUri) { HttpMessageHandler = HttpMessageHandler, }; var authenticationToken = await authManager.TryAcquireCachedTokenAsync(Settings.AccountIdentifier, cancellationToken); Guard.IsNotNull(authenticationToken, nameof(authenticationToken)); InstanceDescriptor = authenticationToken.Account.Username; InstanceDescriptorChanged?.Invoke(this, InstanceDescriptor); var graphClient = authManager.CreateGraphClient(authenticationToken.AccessToken); var driveItem = await graphClient.Drive.Items[Settings.SelectedFolderId].Request().GetAsync(cancellationToken); Guard.IsNotNull(driveItem, nameof(driveItem)); var folderToScan = new OneDriveFolderData(graphClient, driveItem); FileMetadataManager = new FileMetadataManager(folderToScan, _metadataStorage, NotificationService); // Scanning file contents are possible but extremely slow over the network. // The Graph API supplies music metadata from file properties, which is much faster. // Use the user's preferences. var scanTypes = MetadataScanTypes.None; if (Settings.ScanWithTagLib) scanTypes |= MetadataScanTypes.TagLib; if (Settings.ScanWithFileProperties) scanTypes |= MetadataScanTypes.FileProperties; FileMetadataManager.ScanTypes = scanTypes; FileMetadataManager.DegreesOfParallelism = 8; await FileMetadataManager.InitAsync(cancellationToken); _ = FileMetadataManager.ScanAsync(cancellationToken); ChangeCoreState(CoreState.Loaded); Logger.LogInformation("Post config task: setting up generic config UI."); var doneButton = new AbstractButton($"{nameof(GeneralCoreConfigPanel)}DoneButton", "Done"); doneButton.Clicked += GeneralConfigPanelDoneButtonClicked; var ui = new GeneralCoreConfigPanel(Settings, NotificationService) { doneButton }; _configPanel = ui; AbstractConfigPanelChanged?.Invoke(this, EventArgs.Empty); Logger.LogInformation("Initializing library"); await Library.Cast().InitAsync(); void RaiseFailedConnectionState() { NotificationService?.RaiseNotification("Connection failed", "We weren't able to contact OneDrive"); ChangeInstanceDescriptor("Login failed"); } } private void GeneralConfigPanelDoneButtonClicked(object sender, EventArgs e) { ChangeCoreState(CoreState.Configured); ChangeCoreState(CoreState.Loaded); } private async Task AcquireUserSelectedFolderAsync(CancellationToken cancellationToken) { var authManager = new AuthenticationManager(Settings.ClientId, Settings.TenantId, Settings.RedirectUri) { HttpMessageHandler = HttpMessageHandler, }; authManager.MsalAcquireTokenInteractiveParameterBuilderCreated += OnInteractiveParamBuilderCreated; authManager.MsalPublicClientApplicationBuilderCreated += OnPublicClientApplicationBuilderCreated; var authenticationToken = await authManager.TryAcquireCachedTokenAsync(Settings.AccountIdentifier, cancellationToken); if (authenticationToken is null) return null; var graphClient = authManager.CreateGraphClient(authenticationToken.AccessToken); authManager.MsalAcquireTokenInteractiveParameterBuilderCreated -= OnInteractiveParamBuilderCreated; authManager.MsalPublicClientApplicationBuilderCreated -= OnPublicClientApplicationBuilderCreated; var user = await graphClient.Users.Request().GetAsync(cancellationToken); var driveItem = await graphClient.Drive.Root.Request().Expand("children").GetAsync(cancellationToken); var rootFolder = new OneDriveFolderData(graphClient, driveItem); var oobePanel = new OutOfBoxExperiencePanel(Settings, NotificationService, cancellationToken); _configPanel = oobePanel; AbstractConfigPanelChanged?.Invoke(this, EventArgs.Empty); return await oobePanel.ExecuteFolderPickerStageAsync(rootFolder, user.FirstOrDefault()?.DisplayName, Settings.AccountIdentifier); } private async Task AcquireLoginAsync(CancellationToken cancellationToken) { var authManager = new AuthenticationManager(Settings.ClientId, Settings.TenantId, Settings.RedirectUri) { HttpMessageHandler = HttpMessageHandler, }; authManager.MsalAcquireTokenInteractiveParameterBuilderCreated += OnInteractiveParamBuilderCreated; authManager.MsalPublicClientApplicationBuilderCreated += OnPublicClientApplicationBuilderCreated; var authenticationToken = await authManager.TryAcquireCachedTokenAsync(Settings.AccountIdentifier, cancellationToken); if (authenticationToken is null) { var oobePanel = new OutOfBoxExperiencePanel(Settings, NotificationService, cancellationToken); _configPanel = oobePanel; AbstractConfigPanelChanged?.Invoke(this, EventArgs.Empty); if (LoginMethod == LoginMethod.Interactive) { oobePanel.DisplayInteractiveLoginStageAsync(); authenticationToken = await authManager.TryAcquireTokenViaInteractiveLoginAsync(cancellationToken); } else if (LoginMethod == LoginMethod.DeviceCode) { var deviceCodePanel = oobePanel.DisplayDeviceCodeLoginStageAsync(); deviceCodePanel.AuthenticateButton.Clicked += AuthenticateButtonOnClicked; authenticationToken = await authManager.TryAcquireTokenViaDeviceCodeLoginAsync(x => { deviceCodePanel.VerificationUri = new Uri(x.VerificationUrl); deviceCodePanel.Code = x.UserCode; return Task.CompletedTask; }, cancellationToken); deviceCodePanel.AuthenticateButton.Clicked -= AuthenticateButtonOnClicked; void AuthenticateButtonOnClicked(object sender, EventArgs e) => LoginNavigationRequested?.Invoke(this, deviceCodePanel.VerificationUri ?? ThrowHelper.ThrowArgumentNullException(nameof(deviceCodePanel.VerificationUri))); } else throw new ArgumentOutOfRangeException(); } Guard.IsNotNull(authenticationToken, nameof(authenticationToken)); Guard.IsNotNullOrWhiteSpace(authenticationToken.Account.Username, nameof(authenticationToken.Account.Username)); authManager.MsalAcquireTokenInteractiveParameterBuilderCreated -= OnInteractiveParamBuilderCreated; authManager.MsalPublicClientApplicationBuilderCreated -= OnPublicClientApplicationBuilderCreated; return authenticationToken.Account.HomeAccountId.Identifier; } internal void ChangeCoreState(CoreState state) { CoreState = state; CoreStateChanged?.Invoke(this, state); } internal void ChangeInstanceDescriptor(string instanceDescriptor) { InstanceDescriptor = instanceDescriptor; InstanceDescriptorChanged?.Invoke(this, InstanceDescriptor); } private void CompleteGenericSetupButton_Clicked(object sender, EventArgs e) { ChangeCoreState(CoreState.Configured); ChangeCoreState(CoreState.Loaded); } public override ValueTask DisposeAsync() { _completeGenericSetupButton.Clicked -= CompleteGenericSetupButton_Clicked; return base.DisposeAsync(); } private void OnInteractiveParamBuilderCreated(object sender, AcquireTokenInteractiveParameterBuilderCreatedEventArgs args) => MsalAcquireTokenInteractiveParameterBuilderCreated?.Invoke(this, args); private void OnPublicClientApplicationBuilderCreated(object sender, MsalPublicClientApplicationBuilderCreatedEventArgs args) => MsalPublicClientApplicationBuilderCreated?.Invoke(this, args); } }