Merged PR 10748585: Recall | Connect UserActivity to support restoring from snapshots

## What
Thanks to @<Brendan Elliott ⌨>  for his PoC !10573092
This PR is going to use `UserActivity` APIs to connect the app to Recall so that we can take a snapshot when asked and then retore our states from the snapshot later.

## How
- Add a new type `SnapshotLaunchArguments` to identify a protocol launch requested by Recall.
- Add an extension `LaunchExtensions` as helper to retrieve key information from fundamental types.
- Refactor `App::OnActivated()` and `MainPage::OnNavigatedTo()` to handle different protocols properly.
- Create or parse `UserActivity` in `MainPage` in the way like what the PoC is doing.
- Improve the coding style a bit for `MainPage`.

## Note
The serialization work and restoring from JSON is going to be done in a separate PR.

## Testing
Manually tested.
Some typical test cases:
- Launch the app from *Start menu*
- Launch the app from *Task bar*
- Launch the app from *Command line*
- Launch with the protocol for Recall
- Launch with the protocol that is injected with evil data.

Related work items: #50854714
This commit is contained in:
Tian Liao ☕ 2024-05-14 01:51:23 +00:00
commit 04a1842061
4 changed files with 161 additions and 35 deletions

View file

@ -75,13 +75,26 @@ namespace CalculatorApp
{
if (args.Kind == ActivationKind.Protocol)
{
// We currently don't pass the uri as an argument,
// and handle any protocol launch as a normal app launch.
OnAppLaunch(args, null, false);
if (args.IsSnapshotProtocol())
{
var protoArgs = (IProtocolActivatedEventArgs)args;
OnAppLaunch(args,
new SnapshotLaunchArguments
{
ActivityId = protoArgs.Uri.GetActivityId(),
LaunchUri = protoArgs.Uri
},
false);
}
else
{
// handle any unknown protocol launch as a normal app launch.
OnAppLaunch(args, null, false);
}
}
}
private void OnAppLaunch(IActivatedEventArgs args, string argument, bool isPreLaunch)
private void OnAppLaunch(IActivatedEventArgs args, object arguments, bool isPreLaunch)
{
// Uncomment the following lines to display frame-rate and per-frame CPU usage info.
//#if DEBUG
@ -132,7 +145,7 @@ namespace CalculatorApp
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
if (rootFrame.Content == null && !rootFrame.Navigate(typeof(MainPage), argument))
if (rootFrame.Content == null && !rootFrame.Navigate(typeof(MainPage), arguments))
{
// We couldn't navigate to the main page, kill the app so we have a good
// stack to debug

View file

@ -144,6 +144,7 @@
<Compile Include="Common\AlwaysSelectedCollectionView.cs" />
<Compile Include="Common\AppLifecycleLogger.cs" />
<Compile Include="Common\KeyboardShortcutManager.cs" />
<Compile Include="Common\LaunchArguments.cs" />
<Compile Include="Common\ValidatingConverters.cs" />
<Compile Include="Controls\CalculationResult.cs" />
<Compile Include="Controls\CalculationResultAutomationPeer.cs" />

View file

@ -0,0 +1,56 @@
using System;
using System.Linq;
using Windows.ApplicationModel.Activation;
using Windows.ApplicationModel.UserActivities;
namespace CalculatorApp
{
internal class SnapshotLaunchArguments
{
public string ActivityId { get; set; }
public Uri LaunchUri { get; set; }
}
internal static class LaunchExtensions
{
public static bool IsSnapshotProtocol(this IActivatedEventArgs args) =>
args is IProtocolActivatedEventArgs protoArgs &&
protoArgs.Uri != null &&
protoArgs.Uri.AbsolutePath == "/snapshot" &&
!string.IsNullOrEmpty(protoArgs.Uri.Query);
/// <summary>
/// GetActivityId() requires the parameter `launchUri` to be a well-formed
/// snapshot URI.
/// </summary>
/// <param name="launchUri"></param>
/// <returns></returns>
public static string GetActivityId(this Uri launchUri)
{
const string ActivityIdKey = "activityId=";
var segment = launchUri.Query.Split('?', '&').FirstOrDefault(x => x.StartsWith(ActivityIdKey));
if (segment != null)
{
segment = segment.Trim();
return segment.Length > ActivityIdKey.Length ?
segment.Substring(ActivityIdKey.Length) :
string.Empty;
}
return string.Empty;
}
public static bool VerifyIncomingActivity(this SnapshotLaunchArguments launchArgs, UserActivity activity)
{
if (string.IsNullOrEmpty(activity.ActivityId) ||
activity.ActivationUri == null ||
activity.ActivationUri.AbsolutePath != "/snapshot" ||
string.IsNullOrEmpty(activity.ActivationUri.Query) ||
activity.ContentInfo == null)
{
return false;
}
return activity.ActivityId == GetActivityId(launchArgs.LaunchUri);
}
}
}

View file

@ -1,13 +1,9 @@
using CalculatorApp.Common;
using CalculatorApp.Converters;
using CalculatorApp.ViewModel;
using CalculatorApp.ViewModel.Common;
using CalculatorApp.ViewModel.Common.Automation;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Windows.ApplicationModel.UserActivities;
using Windows.Data.Json;
using Windows.Foundation;
using Windows.Graphics.Display;
using Windows.Storage;
@ -15,19 +11,19 @@ using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Automation;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Navigation;
using Microsoft.UI.Xaml.Controls;
using MUXC = Microsoft.UI.Xaml.Controls;
using CalculatorApp.Common;
using CalculatorApp.Converters;
using CalculatorApp.ViewModel;
using CalculatorApp.ViewModel.Common;
using CalculatorApp.ViewModel.Common.Automation;
namespace CalculatorApp
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
public sealed partial class MainPage : Windows.UI.Xaml.Controls.Page
{
public static readonly DependencyProperty NavViewCategoriesSourceProperty =
DependencyProperty.Register(nameof(NavViewCategoriesSource), typeof(List<object>), typeof(MainPage), new PropertyMetadata(default));
@ -59,6 +55,30 @@ namespace CalculatorApp
DisplayInformation.AutoRotationPreferences = DisplayOrientations.Portrait | DisplayOrientations.PortraitFlipped;
}
}
UserActivityRequestManager.GetForCurrentView().UserActivityRequested += async (_, args) =>
{
var deferral = args.GetDeferral();
if (deferral == null)
{
// Windows Bug in ni_moment won't return the deferral propoerly, see https://microsoft.visualstudio.com/DefaultCollection/OS/_workitems/edit/47775705/
return;
}
var channel = UserActivityChannel.GetDefault();
var activity = await channel.GetOrCreateUserActivityAsync($"{Guid.NewGuid()}");
activity.ActivationUri = new Uri($"ms-calculator:///snapshot?activityId={activity.ActivityId}");
var snapshot = "{}"; // TODO: serialize the current snapshot into a JSON representation string.
activity.ContentInfo = UserActivityContentInfo.FromJson(snapshot);
var resProvider = AppResourceProvider.GetInstance();
activity.VisualElements.DisplayText =
$"{resProvider.GetResourceString("AppName")} - {resProvider.GetResourceString(NavCategoryStates.GetNameResourceKey(Model.Mode))}";
await activity.SaveAsync();
args.Request.SetUserActivity(activity);
deferral.Complete();
};
}
public void UnregisterEventHandlers()
@ -121,23 +141,59 @@ namespace CalculatorApp
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewMode initialMode = ViewMode.Standard;
string stringParameter = (e.Parameter as string);
if (!string.IsNullOrEmpty(stringParameter))
var initialMode = ViewMode.Standard;
var localSettings = ApplicationData.Current.LocalSettings;
if (localSettings.Values.ContainsKey(ApplicationViewModel.ModePropertyName))
{
initialMode = (ViewMode)Convert.ToInt32(stringParameter);
initialMode = NavCategoryStates.Deserialize(localSettings.Values[ApplicationViewModel.ModePropertyName]);
}
if (e.Parameter == null)
{
Model.Initialize(initialMode);
return;
}
if (e.Parameter is string legacyArgs)
{
if (legacyArgs.Length > 0)
{
initialMode = (ViewMode)Convert.ToInt32(legacyArgs);
}
Model.Initialize(initialMode);
}
else if (e.Parameter is SnapshotLaunchArguments snapshotArgs)
{
_ = Window.Current.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
{
var channel = UserActivityChannel.GetDefault();
var activity = await channel.GetOrCreateUserActivityAsync(snapshotArgs.ActivityId);
if (!snapshotArgs.VerifyIncomingActivity(activity))
{
// something's going wrong with the activity
// TODO: show error dialog
return;
}
else
{
if (JsonObject.TryParse(activity.ContentInfo.ToJson(), out var jsonModel))
{
// TODO: try restore the model from jsonModel
}
else
{
// data corrupted
// TODO: show error dialog
return;
}
}
});
Model.Initialize(initialMode);
}
else
{
ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings;
if (localSettings.Values.ContainsKey(ApplicationViewModel.ModePropertyName))
{
initialMode = NavCategoryStates.Deserialize(localSettings.Values[ApplicationViewModel.ModePropertyName]);
}
Environment.FailFast("cd75d5af-0f47-4cc2-910c-ed792ed16fe6");
}
Model.Initialize(initialMode);
}
private void InitializeNavViewCategoriesSource()
@ -302,13 +358,13 @@ namespace CalculatorApp
NavView.SetValue(KeyboardShortcutManager.VirtualKeyControlChordProperty, MyVirtualKey.E);
}
private void OnNavPaneOpened(MUXC.NavigationView sender, object args)
private void OnNavPaneOpened(NavigationView sender, object args)
{
KeyboardShortcutManager.HonorShortcuts(false);
TraceLogger.GetInstance().LogNavBarOpened();
}
private void OnNavPaneClosed(MUXC.NavigationView sender, object args)
private void OnNavPaneClosed(NavigationView sender, object args)
{
if (Popup.IsOpen)
{
@ -360,7 +416,7 @@ namespace CalculatorApp
KeyboardShortcutManager.HonorShortcuts(!NavView.IsPaneOpen);
}
private void OnNavSelectionChanged(object sender, MUXC.NavigationViewSelectionChangedEventArgs e)
private void OnNavSelectionChanged(object sender, NavigationViewSelectionChangedEventArgs e)
{
if (e.IsSettingsSelected)
{
@ -368,13 +424,13 @@ namespace CalculatorApp
return;
}
if (e.SelectedItemContainer is MUXC.NavigationViewItem item)
if (e.SelectedItemContainer is NavigationViewItem item)
{
Model.Mode = (ViewMode)item.Tag;
}
}
private void OnNavItemInvoked(MUXC.NavigationView sender, MUXC.NavigationViewItemInvokedEventArgs e)
private void OnNavItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs e)
{
NavView.IsPaneOpen = false;
}