mirror of
https://github.com/Microsoft/calculator.git
synced 2025-08-20 05:13:11 -07:00
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:
parent
d6140d237d
commit
04a1842061
4 changed files with 161 additions and 35 deletions
|
@ -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
|
||||
|
|
|
@ -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" />
|
||||
|
|
56
src/Calculator/Common/LaunchArguments.cs
Normal file
56
src/Calculator/Common/LaunchArguments.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue