Rehydrate for Recall (#2176)

* 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

* Merged PR 10741448: [Recall] Snapshot saving and restoring

####What
According to PM [Spec](https://microsoft.sharepoint-df.com/:w:/t/PAXEssentialExperiences421/EcpP5tGRtFdIsRrP84ueRfUBjb6tfayxWtF9ujvJuNx6Dg?e=AeRzVf), saving and restoring Calculator snapshot when required.

The current snapshot supports to:
- Restore the calculator mode.
- Restore the current calculation (display value and expression).
- Restore the history of calculations (either in Standard mode or Scientific mode) shown at the time the snapshot was taken.
- Restore the current calculation error state, if applicable.

####How
- Added `SnapshotHelper` to help save and restore snapshots.
- Besides the existing snapshot information from view models, added an extra field `SnapshotVersion` in `ApplicationSnapshot` for backward compatibility.

#### Note
Unit tests will be added in a separate PR.

Related work items: #50701758

* Merged PR 10772614: Recall | Update the LaunchURI design

## What
Since `System.Uri` already has the `Segment` property which contains the parsed path blocks, Query in Uri looks too heavy and intrusive in implementation to retrieve the activity id.

## Changes
Changed the launch URI from something like `ms-calculator:///snapshot?activityId=<a guid>` to `ms-calculator:snapshots/<a guid>`

Related work items: #50854714

* Merged PR 10778666: Recall | Show error dialog if launching from snapshot has failed

## What
Per Figma design, we can show an error dialog with messages for the failures happens during snapshot launch.

## Notes
- Fixed a crash about taking a snapshot when Calculator hasn't initialized a standard calculator.
- Simplified the restore path.

## Screenshot
![image.png](https://dev.azure.com/microsoft/d1a24475-535d-4c83-988a-9491e6dbc858/_apis/git/repositories/6255259e-4ead-4d8d-b165-55eeacc5ca48/pullRequests/10778666/attachments/image.png)

Related work items: #50858262

* Merged PR 10790341: [Recall] Update calculator engine with snapshot for further calculations

####What
When restoring from snapshot, we need to set calculator engine properly to make further calculations correct.

####How
Update calculator engine by a serial of corresponding commands from snapshot.

To get the commands for the display area when saving snapshot,
1. If the expression is not from history and the primary display is the result of the expression, `DisplayCommands` of `StandardCalculatorSnapshot` will be empty, and we will use the commands from `ExpressionDisplay` for restoring in the future.
2. If the expression is not from history and the primary display is not the result of the expression, `DisplayCommands` of `StandardCalculatorSnapshot` will be the commands from the history collector in addition to the operand command in the primary display, and it will be used for restoring in the future.
3. If the expression and primary display are from history, `DisplayCommands` will be incomplete with the operand command in the primary display missing as by current design of history, and the commands from `ExpressionDisplay` will be used for restoring in the future.

Related work items: #51002745

* Merged PR 10802927: Recall | Add threat model

## What
The support for Recall introduced a URI activation process allowing the Recall app to launch Calculator with desired snapshot metadata. This led to a potential security problem, and we need to address it by providing justifications.

Threat model is a well-known practice among Inbox Apps. After offline discussion, we decide to add diagrams for this Recall feature to prepare for the security review.

## Diagram preview
![image.png](https://dev.azure.com/microsoft/d1a24475-535d-4c83-988a-9491e6dbc858/_apis/git/repositories/6255259e-4ead-4d8d-b165-55eeacc5ca48/pullRequests/10802927/attachments/image.png)

![image (2).png](https://dev.azure.com/microsoft/d1a24475-535d-4c83-988a-9491e6dbc858/_apis/git/repositories/6255259e-4ead-4d8d-b165-55eeacc5ca48/pullRequests/10802927/attachments/image%20%282%29.png)

Related work items: #51165486

* Merged PR 10794979: Add Recall Telemetry

**Snapshot**
EventName: `RecallSnapshot`
Payload example:` { "CalcMode": "Standard"}`
**_Fires when a snapshot (UserActivityRequested) is triggered_**

**Restore**
EventName: `RecallRestore`
Payload example:`{ "CalcMode": "Standard"}`
**_Fires when launching by snapshot (recall restore)**

**Error**
EventName: `Exception`
Payload example: `{ "CalcMode": "Standard", "FunctionName" : "MainPage::ShowSnapshotLaunchErrorAsync", "Message": "SnapshotRestoreError" }`
**_Fires when launching by snapshot failed_**

Related work items: #51114542

---------

Co-authored-by: Tian Liao  <tilia@microsoft.com>
Co-authored-by: Jian Zhang <zjian@microsoft.com>
This commit is contained in:
hanzhang54 2024-05-28 16:08:27 +08:00 committed by GitHub
commit 90af5adcd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1106 additions and 140 deletions

View file

@ -1,13 +1,10 @@
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 System.Threading.Tasks;
using Windows.ApplicationModel.UserActivities;
using Windows.Data.Json;
using Windows.Foundation;
using Windows.Graphics.Display;
using Windows.Storage;
@ -15,19 +12,21 @@ 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;
using wuxc = Windows.UI.Xaml.Controls;
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 : wuxc.Page
{
public static readonly DependencyProperty NavViewCategoriesSourceProperty =
DependencyProperty.Register(nameof(NavViewCategoriesSource), typeof(List<object>), typeof(MainPage), new PropertyMetadata(default));
@ -59,6 +58,28 @@ 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:snapshots/{activity.ActivityId}");
activity.ContentInfo = UserActivityContentInfo.FromJson(Model.SaveApplicationSnapshot().Stringify());
activity.IsRoamable = false;
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();
TraceLogger.GetInstance().LogRecallSnapshot(Model.Mode);
};
}
public void UnregisterEventHandlers()
@ -121,23 +142,83 @@ 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 (TryRestoreFromActivity(snapshotArgs, activity, out var errorMessage))
{
TraceLogger.GetInstance().LogRecallRestore(Model.Mode);
SelectNavigationItemByModel();
}
else
{
TraceLogger.GetInstance().LogRecallError(Model.Mode, errorMessage);
await ShowSnapshotLaunchErrorAsync();
}
});
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");
}
}
private bool TryRestoreFromActivity(SnapshotLaunchArguments snapshotArgs, UserActivity activity, out string errorMessage)
{
if (!snapshotArgs.VerifyIncomingActivity(activity))
{
errorMessage = "IncomingActivityFailed";
return false;
}
Model.Initialize(initialMode);
// Work around for bug https://microsoft.visualstudio.com/DefaultCollection/OS/_workitems/edit/48931227
// where ContentInfo can't be directly accessed.
if (!JsonObject.TryParse(activity.ToJson(), out var activityJson))
{
errorMessage = "ParseJsonError";
return false;
}
if (!activityJson.ContainsKey("contentInfo"))
{
errorMessage = "ContentInfoNotExist";
return false;
}
if (!Model.TryRestoreFromSnapshot(activityJson.GetNamedObject("contentInfo")))
{
errorMessage = "RestoreFromSnapshotFailed";
return false;
}
errorMessage = string.Empty;
return true;
}
private void InitializeNavViewCategoriesSource()
@ -302,13 +383,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 +441,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 +449,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;
}
@ -610,6 +691,19 @@ namespace CalculatorApp
CloseSettingsPopup();
}
private async Task ShowSnapshotLaunchErrorAsync()
{
var resProvider = AppResourceProvider.GetInstance();
var dialog = new wuxc.ContentDialog
{
Title = resProvider.GetResourceString("AppName"),
Content = new wuxc.TextBlock { Text = resProvider.GetResourceString("SnapshotRestoreError") },
CloseButtonText = resProvider.GetResourceString("ErrorButtonOk"),
DefaultButton = wuxc.ContentDialogButton.Close
};
await dialog.ShowAsync();
}
private Calculator m_calculator;
private GraphingCalculator m_graphingCalculator;
private UnitConverter m_converter;