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

display notifications for updates #1360

Merged
merged 3 commits into from
Jan 13, 2025
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
1 change: 1 addition & 0 deletions backend/FwLite/FwLiteMaui/FwLiteMaui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)"/>
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="NReco.Logging.File" Version="1.2.1" />
<PackageReference Include="System.Collections" Version="4.3.0" />
<PackageReference Include="System.IO" Version="4.3.0" />
Expand Down
5 changes: 5 additions & 0 deletions backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Diagnostics;
using System.Reflection;
using FwLiteShared;
using FwLiteShared.Auth;
using LcmCrdt;
Expand Down Expand Up @@ -78,6 +80,9 @@ public static void AddFwLiteMauiServices(this IServiceCollection services,
});

var defaultDataPath = IsPortableApp ? Directory.GetCurrentDirectory() : FileSystem.AppDataDirectory;
//when launching from a notification, the current directory may be C:\Windows\System32, so we'll use the path of the executable instead
if (defaultDataPath.StartsWith("C:\\Windows\\System32", StringComparison.OrdinalIgnoreCase))
defaultDataPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName ?? Assembly.GetExecutingAssembly().Location) ?? ".";
var baseDataPath = Path.GetFullPath(configuration.GetSection("FwLiteMaui").GetValue<string>("BaseDataDir") ??
defaultDataPath);
logging.AddFilter("FwLiteShared.Auth.LoggerAdapter", LogLevel.Warning);
Expand Down
111 changes: 107 additions & 4 deletions backend/FwLite/FwLiteMaui/Platforms/Windows/AppUpdateService.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Buffers;
using System.Net.Http.Json;
using System.Text.Json;
using Windows.Management.Deployment;
using Windows.Networking.Connectivity;
using LexCore.Entities;
using Microsoft.Extensions.Logging;
using Microsoft.Toolkit.Uwp.Notifications;

namespace FwLiteMaui;

Expand All @@ -16,6 +19,10 @@ public class AppUpdateService(
private const string FwliteUpdateUrlEnvVar = "FWLITE_UPDATE_URL";
private const string ForceUpdateCheckEnvVar = "FWLITE_FORCE_UPDATE_CHECK";
private const string PreventUpdateCheckEnvVar = "FWLITE_PREVENT_UPDATE";
private const string NotificationIdKey = "notificationId";
private const string ActionKey = "action";
private const string ResultRefKey = "resultRef";
private static readonly Dictionary<string, TaskCompletionSource<string?>> NotificationCompletionSources = new();

private static readonly SearchValues<string> ValidPositiveEnvVarValues =
SearchValues.Create(["1", "true", "yes"], StringComparison.OrdinalIgnoreCase);
Expand All @@ -25,6 +32,16 @@ public class AppUpdateService(

public void Initialize(IServiceProvider services)
{
ToastNotificationManagerCompat.OnActivated += toastArgs =>
{
ToastArguments args = ToastArguments.Parse(toastArgs.Argument);
HandleNotificationAction(args.Get(ActionKey), args.Get(NotificationIdKey), args);
};
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
//don't check for updates if the user already clicked on a notification
return;
}
_ = Task.Run(TryUpdate);
}

Expand All @@ -39,19 +56,90 @@ private async Task TryUpdate()
if (!ShouldCheckForUpdate()) return;
var response = await ShouldUpdate();
if (!response.Update) return;

if (ShouldPromptBeforeUpdate() && !await RequestPermissionToUpdate(response.Release))
{
return;
}
await ApplyUpdate(response.Release);
}

private async Task ApplyUpdate(FwLiteRelease latestRelease)
private async Task Test()
{
logger.LogInformation("Testing update notifications");
var fwLiteRelease = new FwLiteRelease("1.0.0.0", "https://test.com");
if (!await RequestPermissionToUpdate(fwLiteRelease))
{
logger.LogInformation("User declined update");
return;
}

await ApplyUpdate(fwLiteRelease);
}

private void ShowUpdateAvailableNotification(FwLiteRelease latestRelease)
{
new ToastContentBuilder().AddText("FieldWorks Lite Update Available").AddText($"Version {latestRelease.Version} will be installed after FieldWorks Lite is closed").Show();
}

private async Task<bool> RequestPermissionToUpdate(FwLiteRelease latestRelease)
{
var notificationId = $"update-{Guid.NewGuid()}";
var tcs = new TaskCompletionSource<string?>();
NotificationCompletionSources.Add(notificationId, tcs);
new ToastContentBuilder()
.AddText("FieldWorks Lite Update")
.AddText("A new version of FieldWorks Lite is available")
.AddText($"Version {latestRelease.Version} would you like to download and install this update?")
.AddArgument(NotificationIdKey, notificationId)
.AddButton(new ToastButton()
.SetContent("Download & Install")
.AddArgument(ActionKey, "download")
.AddArgument("release", JsonSerializer.Serialize(latestRelease)))
.AddArgument(ResultRefKey, "release")
.Show(toast =>
{
toast.Tag = "update";
});
var taskResult = await tcs.Task;
return taskResult != null;
}

private void HandleNotificationAction(string action, string notificationId, ToastArguments args)
{
var result = args.Get(args.Get(ResultRefKey));
if (!NotificationCompletionSources.TryGetValue(notificationId, out var tcs))
{
if (action == "download")
{
var release = JsonSerializer.Deserialize<FwLiteRelease>(result);
if (release == null)
{
logger.LogError("Invalid release {Release} for notification {NotificationId}", result, notificationId);
return;
}
_ = Task.Run(() => ApplyUpdate(release, true));
}
else
{
logger.LogError("Unknown action {Action} for notification {NotificationId}", action, notificationId);
}
return;
}

tcs.SetResult(result);
NotificationCompletionSources.Remove(notificationId);
}

private async Task ApplyUpdate(FwLiteRelease latestRelease, bool quitOnUpdate = false)
{
logger.LogInformation("New version available: {Version}, Current version: {CurrentVersion}", latestRelease.Version, AppVersion.Version);
logger.LogInformation("Installing new version: {Version}, Current version: {CurrentVersion}", latestRelease.Version, AppVersion.Version);
var packageManager = new PackageManager();
var asyncOperation = packageManager.AddPackageByUriAsync(new Uri(latestRelease.Url),
new AddPackageOptions()
{
DeferRegistrationWhenPackagesAreInUse = true,
ForceUpdateFromAnyVersion = true
ForceUpdateFromAnyVersion = true,
ForceAppShutdown = quitOnUpdate
});
asyncOperation.Progress = (info, progressInfo) =>
{
Expand All @@ -70,6 +158,7 @@ private async Task ApplyUpdate(FwLiteRelease latestRelease)
}

logger.LogInformation("Update downloaded, will install on next restart");
ShowUpdateAvailableNotification(latestRelease);
}

private async Task<ShouldUpdateResponse> ShouldUpdate()
Expand Down Expand Up @@ -124,4 +213,18 @@ private bool ShouldCheckForUpdate()
preferences.Set(LastUpdateCheck, DateTime.UtcNow);
return true;
}

private bool ShouldPromptBeforeUpdate()
{
return IsOnMeteredConnection();
}

private bool IsOnMeteredConnection()
{
var profile = NetworkInformation.GetInternetConnectionProfile();
if (profile == null) return false;
var cost = profile.GetConnectionCost();
return cost.NetworkCostType != NetworkCostType.Unrestricted;

}
}
21 changes: 20 additions & 1 deletion backend/FwLite/FwLiteMaui/Platforms/Windows/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
IgnorableNamespaces="uap rescap com desktop">

<Identity Name="FwLiteMaui"
Publisher="CN=&quot;Summer Institute of Linguistics, Inc.&quot;, O=&quot;Summer Institute of Linguistics, Inc.&quot;, L=Dallas, S=Texas, C=US"
Expand Down Expand Up @@ -37,6 +39,23 @@
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
<Extensions>

<!--Specify which CLSID to activate when toast clicked-->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="49f2053c-31cc-4eb9-8a65-84491e543d56"/>
</desktop:Extension>

<!--Register COM CLSID LocalServer32 registry key-->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="$targetnametoken$.exe" Arguments="-ToastActivated"
DisplayName="Toast activator">
<com:Class Id="49f2053c-31cc-4eb9-8a65-84491e543d56" DisplayName="Toast activator"/>
</com:ExeServer>
</com:ComServer>
</com:Extension>
</Extensions>
</Application>
</Applications>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ public static class WindowsKernel
public static void AddFwLiteWindows(this IServiceCollection services, IHostEnvironment environment)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
services.AddSingleton<IMauiInitializeService, AppUpdateService>();
if (!FwLiteMauiKernel.IsPortableApp)
{
services.AddSingleton<IMauiInitializeService, AppUpdateService>();
services.AddSingleton<IMauiInitializeService, WindowsShortcutService>();
}
services.Configure<FwLiteConfig>(config =>
Expand Down
Loading