Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expanding on App Install URI Protocol Support - Winget Package URIs #2733

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 106 additions & 42 deletions src/Services/AppInstallActivationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using DevHome.Common.Extensions;
using DevHome.Common.Services;
using DevHome.Settings.ViewModels;
using DevHome.SetupFlow.Models;
using DevHome.SetupFlow.Services;
using DevHome.SetupFlow.ViewModels;
using Microsoft.UI.Xaml;
Expand All @@ -17,7 +18,7 @@
namespace DevHome.Services;

/// <summary>
/// Class that handles the activation of the application when an add-apps-to-cart URI protcol is used.
/// Class that handles the activation of the application when an add-apps-to-cart URI protocol is used.
/// </summary>
public class AppInstallActivationHandler : ActivationHandler<ProtocolActivatedEventArgs>
{
Expand All @@ -28,20 +29,32 @@ public class AppInstallActivationHandler : ActivationHandler<ProtocolActivatedEv
private readonly IWindowsPackageManager _windowsPackageManager;
private readonly PackageProvider _packageProvider;
private readonly SetupFlowOrchestrator _setupFlowOrchestrator;
private readonly Window _mainWindow;
private readonly ISetupFlowStringResource _setupFlowStringResource;
private static readonly char[] Separator = [','];

public enum ActivationQueryType
{
Search,
WingetURIs,
}

public AppInstallActivationHandler(
INavigationService navigationService,
SetupFlowViewModel setupFlowViewModel,
PackageProvider packageProvider,
IWindowsPackageManager wpm,
SetupFlowOrchestrator setupFlowOrchestrator)
SetupFlowOrchestrator setupFlowOrchestrator,
ISetupFlowStringResource setupFlowStringResource,
Window mainWindow)
{
_navigationService = navigationService;
_setupFlowViewModel = setupFlowViewModel;
_packageProvider = packageProvider;
_windowsPackageManager = wpm;
_setupFlowOrchestrator = setupFlowOrchestrator;
_setupFlowStringResource = setupFlowStringResource;
_mainWindow = mainWindow;
}

protected override bool CanHandleInternal(ProtocolActivatedEventArgs args)
Expand All @@ -50,64 +63,115 @@ protected override bool CanHandleInternal(ProtocolActivatedEventArgs args)
}

protected async override Task HandleInternalAsync(ProtocolActivatedEventArgs args)
{
await AppActivationFlowAsync(args.Uri.Query);
}
{
var uri = args.Uri;
var parameters = HttpUtility.ParseQueryString(uri.Query);

private async Task AppActivationFlowAsync(string query)
{
try
if (parameters != null)
{
// Don't interrupt the user if the machine configuration is in progress
if (_setupFlowOrchestrator.IsMachineConfigurationInProgress)
{
_log.Warning("Cannot activate the add-apps-to-cart flow because the machine configuration is in progress");
return;
foreach (ActivationQueryType queryType in Enum.GetValues(typeof(ActivationQueryType)))
{
var query = parameters.Get(queryType.ToString());

if (!string.IsNullOrEmpty(query))
{
await AppActivationFlowAsync(query, queryType);
return; // Exit after handling the first non-null query
}
}
else
{
_log.Information("Starting add-apps-to-cart activation");
_navigationService.NavigateTo(typeof(SetupFlowViewModel).FullName!);
_setupFlowViewModel.StartAppManagementFlow(query);
await SearchAndSelectAsync(query);
}
}
catch (Exception ex)
{
_log.Error(ex, "Error executing the add-apps-to-cart activation flow");
}
}

private async Task SearchAndSelectAsync(string query)
private async Task AppActivationFlowAsync(string query, ActivationQueryType queryType)
{
var parameters = HttpUtility.ParseQueryString(query);
var searchParameter = parameters["search"];
if (_setupFlowOrchestrator.IsMachineConfigurationInProgress)
{
_log.Warning($"Cannot activate the {AppSearchUri} flow because the machine configuration is in progress");
await _mainWindow.ShowErrorMessageDialogAsync(
_setupFlowStringResource.GetLocalized(StringResourceKey.AppInstallActivationTitle),
_setupFlowStringResource.GetLocalized(StringResourceKey.URIActivationFailedBusy),
_setupFlowStringResource.GetLocalized(StringResourceKey.Close));
return;
}

if (string.IsNullOrEmpty(searchParameter))
var identifiers = SplitAndTrimIdentifiers(query);
if (identifiers.Length == 0)
{
_log.Warning("Search parameter is missing or empty in the query.");
_log.Warning("No valid identifiers provided in the query.");
return;
}

// Currently using the first search term only
var firstSearchTerm = searchParameter.Split(Separator, StringSplitOptions.RemoveEmptyEntries)
.Select(term => term.Trim(' ', '"'))
.FirstOrDefault();
_log.Information($"Starting {AppSearchUri} activation");
_navigationService.NavigateTo(typeof(SetupFlowViewModel).FullName!);
_setupFlowViewModel.StartAppManagementFlow(queryType == ActivationQueryType.Search ? identifiers[0] : null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why null if ActivationQueryType isn't search?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's because we only want to do the query prefilling into the search box piece in the search scenario.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to the other comment, lmk if you think using a bool prefillSearch might help with clarity here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if null is passed anywhere else into StartAppManagementFlow? MainPageViewModel.StartAppManagementFlow throws if the string is null when trying to log the query.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the only place we call StartAppManagementFlow -- it was added with the previous PR: https://github.com/microsoft/devhome/pull/2642/files

With the change to not log the query string, StartAppManagementFlow should not throw if null (tested locally to confirm)

await HandleAppSelectionAsync(identifiers, queryType);
}

private string[] SplitAndTrimIdentifiers(string query)
{
return query.Split(Separator, StringSplitOptions.RemoveEmptyEntries)
.Select(id => id.Trim(' ', '"'))
.ToArray();
}

if (string.IsNullOrEmpty(firstSearchTerm))
private async Task HandleAppSelectionAsync(string[] identifiers, ActivationQueryType queryType)
{
try
{
switch (queryType)
{
case ActivationQueryType.Search:
await SearchAndSelectAsync(identifiers[0]);
return;

case ActivationQueryType.WingetURIs:
await PackageSearchAsync(identifiers);
return;
}
}
catch (Exception ex)
{
_log.Warning("No valid search term was extracted from the query.");
return;
_log.Error(ex, $"Error executing the {AppSearchUri} activation flow");
}
}

private async Task PackageSearchAsync(string[] identifiers)
{
List<WinGetPackageUri> uris = [];

var searchResults = await _windowsPackageManager.SearchAsync(firstSearchTerm, 1);
foreach (var identifier in identifiers)
{
uris.Add(new WinGetPackageUri(identifier));
}

try
{
var list = await _windowsPackageManager.GetPackagesAsync(uris);
foreach (var item in list)
{
var package = _packageProvider.CreateOrGet(item);
package.IsSelected = true;
_log.Information($"Selected package: {item} for addition to cart.");
}
}
catch (Exception ex)
{
_log.Error(ex, $"Error occurred during package search for URIs: {uris}.");
}
}

private async Task SearchAndSelectAsync(string identifier)
{
var searchResults = await _windowsPackageManager.SearchAsync(identifier, 1);
if (searchResults.Count == 0)
{
_log.Warning("No results found for the search term: {SearchTerm}", firstSearchTerm);
return;
_log.Warning($"No results found for the identifier: {identifier}");
}
else
{
var package = _packageProvider.CreateOrGet(searchResults[0]);
package.IsSelected = true;
_log.Information($"Selected package: {package} for addition to cart.");
}

var firstResult = _packageProvider.CreateOrGet(searchResults[0]);
firstResult.IsSelected = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static class StringResourceKey
public static readonly string AddAllApplications = nameof(AddAllApplications);
public static readonly string AddApplication = nameof(AddApplication);
public static readonly string AddedApplication = nameof(AddedApplication);
public static readonly string AppInstallActivationTitle = nameof(AppInstallActivationTitle);
public static readonly string ApplicationsAddedPlural = nameof(ApplicationsAddedPlural);
public static readonly string ApplicationsAddedSingular = nameof(ApplicationsAddedSingular);
public static readonly string Applications = nameof(Applications);
Expand Down Expand Up @@ -85,6 +86,7 @@ public static class StringResourceKey
public static readonly string SelectedPackagesCount = nameof(SelectedPackagesCount);
public static readonly string SetUpButton = nameof(SetUpButton);
public static readonly string SizeWithColon = nameof(SizeWithColon);
public static readonly string URIActivationFailedBusy = nameof(URIActivationFailedBusy);
public static readonly string LoadingPageHeaderLocalText = nameof(LoadingPageHeaderLocalText);
public static readonly string LoadingPageHeaderTargetText = nameof(LoadingPageHeaderTargetText);
public static readonly string LoadingPageSetupTargetText = nameof(LoadingPageSetupTargetText);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
<value>Select packages to restore from your backup, clone from repository, or search with Windows Package Manager.</value>
<comment>Description for Add packages page</comment>
</data>
<data name="AppInstallActivationTitle" xml:space="preserve">
<value>Configuration in progress</value>
<comment>Title displayed to the user when they attempt to activate the app through a URI but the machine configuration flow is currently in progress.</comment>
</data>
<data name="AppListBackupBanner.Title" xml:space="preserve">
<value>Transfer developer machine settings</value>
<comment>Title text of an instruction banner section for transferring settings from a developer machine</comment>
Expand Down Expand Up @@ -1199,6 +1203,10 @@
<value>Enter the full path and folder name</value>
<comment>Error message to tell the user to fully qualify their clone path</comment>
</data>
<data name="URIActivationFailedBusy" xml:space="preserve">
<value>The URI activation flow cannot be initiated while machine configuration is in progress. Please complete or cancel your current configuration tasks and then try again.</value>
<comment>Message displayed to the user when they attempt to activate the app through a URI but the machine configuration flow is currently in progress.</comment>
</data>
<data name="UrlValidationBadUrl" xml:space="preserve">
<value>Enter a repository name beginning with https://</value>
<comment>Error string to show the user if the url is not an absolute URL</comment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ public AppManagementTaskGroup(

public void HandleSearchQuery(string query)
{
var searchParameter = HttpUtility.ParseQueryString(query)["search"];
if (!string.IsNullOrEmpty(searchParameter))
{
var trimmedSearchParameter = searchParameter.Trim('\"');
_appManagementViewModel.PerformSearch(trimmedSearchParameter);
}
_appManagementViewModel.PerformSearch(query);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,11 @@ public async Task StartConfigurationFileAsync(StorageFile file)
}
}

internal void StartAppManagementFlow(string query)
internal void StartAppManagementFlow(string query = null)
{
_log.Information($"Launching app management flow for query:{query}");
_log.Information("Launching app management flow");
var appManagementSetupFlow = _host.GetService<AppManagementTaskGroup>();
StartSetupFlowForTaskGroups(null, "App Search URI", appManagementSetupFlow);
StartSetupFlowForTaskGroups(null, "App Activation URI", appManagementSetupFlow);
appManagementSetupFlow.HandleSearchQuery(query);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public void OnNavigatedTo(NavigationEventArgs args)
}
}

public void StartAppManagementFlow(string query)
public void StartAppManagementFlow(string query = null)
{
Orchestrator.FlowPages = [_mainPageViewModel];
_mainPageViewModel.StartAppManagementFlow(query);
Expand Down
Loading