Skip to content

Commit f2301cf

Browse files
Merge pull request #3567 from windows-toolkit/aleader/win32-scheduled-notif-fix
Win32 scheduled notification and Setting fixes
2 parents 5424989 + 8cf414a commit f2301cf

File tree

7 files changed

+392
-27
lines changed

7 files changed

+392
-27
lines changed

Microsoft.Toolkit.Uwp.Notifications/Toasts/Compat/Desktop/Win32AppInfo.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
#if WIN32
66

77
using System;
8+
using System.Collections.Specialized;
89
using System.Diagnostics;
910
using System.Drawing;
1011
using System.Drawing.Imaging;
1112
using System.IO;
13+
using System.Linq;
1214
using System.Reflection;
1315
using System.Runtime.InteropServices;
1416
using System.Security.Cryptography;
@@ -18,6 +20,11 @@ namespace Microsoft.Toolkit.Uwp.Notifications
1820
{
1921
internal class Win32AppInfo
2022
{
23+
/// <summary>
24+
/// If an AUMID is greater than 129 characters, scheduled toast notification APIs will throw an exception.
25+
/// </summary>
26+
private const int AUMID_MAX_LENGTH = 129;
27+
2128
public string Aumid { get; set; }
2229

2330
public string DisplayName { get; set; }
@@ -32,6 +39,9 @@ public static Win32AppInfo Get()
3239
IApplicationResolver appResolver = (IApplicationResolver)new CAppResolver();
3340
appResolver.GetAppIDForProcess(Convert.ToUInt32(process.Id), out string appId, out _, out _, out _);
3441

42+
// Use app ID (or hashed app ID) as AUMID
43+
string aumid = appId.Length > AUMID_MAX_LENGTH ? HashAppId(appId) : appId;
44+
3545
// Then try to get the shortcut (for display name and icon)
3646
IShellItem shortcutItem = null;
3747
try
@@ -98,12 +108,21 @@ public static Win32AppInfo Get()
98108

99109
return new Win32AppInfo()
100110
{
101-
Aumid = appId,
111+
Aumid = aumid,
102112
DisplayName = displayName,
103113
IconPath = iconPath
104114
};
105115
}
106116

117+
private static string HashAppId(string appId)
118+
{
119+
using (SHA1 sha1 = SHA1.Create())
120+
{
121+
byte[] hashedBytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(appId));
122+
return string.Join(string.Empty, hashedBytes.Select(b => b.ToString("X2")));
123+
}
124+
}
125+
107126
private static string GetDisplayNameFromCurrentProcess(Process process)
108127
{
109128
// If AssemblyTitle is set, use that

Microsoft.Toolkit.Uwp.Notifications/Toasts/Compat/ToastNotificationHistoryCompat.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,18 @@ public IReadOnlyList<ToastNotification> GetHistory()
5353
/// <param name="tag">The tag label of the toast notification to be removed.</param>
5454
public void Remove(string tag)
5555
{
56+
#if WIN32
5657
if (_aumid != null)
5758
{
58-
_history.Remove(tag, string.Empty, _aumid);
59+
_history.Remove(tag, ToastNotificationManagerCompat.DEFAULT_GROUP, _aumid);
5960
}
6061
else
6162
{
6263
_history.Remove(tag);
6364
}
65+
#else
66+
_history.Remove(tag);
67+
#endif
6468
}
6569

6670
/// <summary>

Microsoft.Toolkit.Uwp.Notifications/Toasts/Compat/ToastNotificationManagerCompat.cs

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public static class ToastNotificationManagerCompat
2626
{
2727
#if WIN32
2828
private const string TOAST_ACTIVATED_LAUNCH_ARG = "-ToastActivated";
29+
private const string REG_HAS_SENT_NOTIFICATION = "HasSentNotification";
30+
internal const string DEFAULT_GROUP = "toolkitGroupNull";
2931

3032
private const int CLASS_E_NOAGGREGATION = -2147221232;
3133
private const int E_NOINTERFACE = -2147467262;
@@ -37,6 +39,8 @@ public static class ToastNotificationManagerCompat
3739
private static bool _registeredOnActivated;
3840
private static List<OnActivated> _onActivated = new List<OnActivated>();
3941

42+
private static bool _hasSentNotification;
43+
4044
/// <summary>
4145
/// Event that is triggered when a notification or notification button is clicked. Subscribe to this event in your app's initial startup code.
4246
/// </summary>
@@ -145,7 +149,7 @@ private static void Initialize()
145149
var activatorType = CreateAndRegisterActivator();
146150

147151
// Register via registry
148-
using (var rootKey = Registry.CurrentUser.CreateSubKey(@"Software\Classes\AppUserModelId\" + _win32Aumid))
152+
using (var rootKey = Registry.CurrentUser.CreateSubKey(GetRegistrySubKey()))
149153
{
150154
// If they don't have identity, we need to specify the display assets
151155
if (!DesktopBridgeHelpers.HasIdentity())
@@ -168,12 +172,20 @@ private static void Initialize()
168172
// Background color only appears in the settings page, format is
169173
// hex without leading #, like "FFDDDDDD"
170174
rootKey.SetValue("IconBackgroundColor", "FFDDDDDD");
175+
176+
// Additionally, we need to read whether they've sent a notification before
177+
_hasSentNotification = rootKey.GetValue(REG_HAS_SENT_NOTIFICATION) != null;
171178
}
172179

173180
rootKey.SetValue("CustomActivator", string.Format("{{{0}}}", activatorType.GUID));
174181
}
175182
}
176183

184+
private static string GetRegistrySubKey()
185+
{
186+
return @"Software\Classes\AppUserModelId\" + _win32Aumid;
187+
}
188+
177189
private static Type CreateActivatorType()
178190
{
179191
// https://stackoverflow.com/questions/24069352/c-sharp-typebuilder-generate-class-with-function-dynamically
@@ -332,8 +344,8 @@ private static extern int CoRegisterClassObject(
332344
/// <summary>
333345
/// Creates a toast notifier.
334346
/// </summary>
335-
/// <returns><see cref="ToastNotifier"/></returns>
336-
public static ToastNotifier CreateToastNotifier()
347+
/// <returns><see cref="ToastNotifierCompat"/>An instance of the toast notifier.</returns>
348+
public static ToastNotifierCompat CreateToastNotifier()
337349
{
338350
#if WIN32
339351
if (_initializeEx != null)
@@ -343,14 +355,14 @@ public static ToastNotifier CreateToastNotifier()
343355

344356
if (DesktopBridgeHelpers.HasIdentity())
345357
{
346-
return ToastNotificationManager.CreateToastNotifier();
358+
return new ToastNotifierCompat(ToastNotificationManager.CreateToastNotifier());
347359
}
348360
else
349361
{
350-
return ToastNotificationManager.CreateToastNotifier(_win32Aumid);
362+
return new ToastNotifierCompat(ToastNotificationManager.CreateToastNotifier(_win32Aumid));
351363
}
352364
#else
353-
return ToastNotificationManager.CreateToastNotifier();
365+
return new ToastNotifierCompat(ToastNotificationManager.CreateToastNotifier());
354366
#endif
355367
}
356368

@@ -437,7 +449,7 @@ public static void Uninstall()
437449
// Remove registry key
438450
if (_win32Aumid != null)
439451
{
440-
Registry.CurrentUser.DeleteSubKey(@"Software\Classes\AppUserModelId\" + _win32Aumid);
452+
Registry.CurrentUser.DeleteSubKey(GetRegistrySubKey());
441453
}
442454
}
443455
catch
@@ -472,6 +484,51 @@ public static void Uninstall()
472484
}
473485
}
474486
#endif
487+
488+
#if WIN32
489+
internal static void SetHasSentToastNotification()
490+
{
491+
// For plain Win32 apps, record that we've sent a notification
492+
if (!_hasSentNotification && !DesktopBridgeHelpers.HasIdentity())
493+
{
494+
_hasSentNotification = true;
495+
496+
try
497+
{
498+
using (var rootKey = Registry.CurrentUser.CreateSubKey(GetRegistrySubKey()))
499+
{
500+
rootKey.SetValue(REG_HAS_SENT_NOTIFICATION, 1);
501+
}
502+
}
503+
catch
504+
{
505+
}
506+
}
507+
}
508+
509+
internal static void PreRegisterIdentityLessApp()
510+
{
511+
// For plain Win32 apps, we first have to have send a toast notification once before using scheduled toasts.
512+
if (!_hasSentNotification && !DesktopBridgeHelpers.HasIdentity())
513+
{
514+
const string tag = "toolkit1stNotif";
515+
516+
// Show the toast
517+
new ToastContentBuilder()
518+
.AddText("New notification")
519+
.Show(toast =>
520+
{
521+
// We'll hide the popup and set the toast to expire in case removing doesn't work
522+
toast.SuppressPopup = true;
523+
toast.Tag = tag;
524+
toast.ExpirationTime = DateTime.Now.AddSeconds(15);
525+
});
526+
527+
// And then remove it
528+
ToastNotificationManagerCompat.History.Remove(tag);
529+
}
530+
}
531+
#endif
475532
}
476533
}
477534

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#if WINDOWS_UWP
6+
7+
using System.Collections.Generic;
8+
using Windows.UI.Notifications;
9+
10+
namespace Microsoft.Toolkit.Uwp.Notifications
11+
{
12+
/// <summary>
13+
/// Allows you to show and schedule toast notifications.
14+
/// </summary>
15+
public sealed class ToastNotifierCompat
16+
{
17+
private ToastNotifier _notifier;
18+
19+
internal ToastNotifierCompat(ToastNotifier notifier)
20+
{
21+
_notifier = notifier;
22+
}
23+
24+
/// <summary>
25+
/// Displays the specified toast notification.
26+
/// </summary>
27+
/// <param name="notification">The object that contains the content of the toast notification to display.</param>
28+
public void Show(ToastNotification notification)
29+
{
30+
#if WIN32
31+
PreprocessToast(notification);
32+
#endif
33+
34+
_notifier.Show(notification);
35+
36+
#if WIN32
37+
ToastNotificationManagerCompat.SetHasSentToastNotification();
38+
#endif
39+
}
40+
41+
/// <summary>
42+
/// Hides the specified toast notification from the screen (moves it into Action Center).
43+
/// </summary>
44+
/// <param name="notification">The object that specifies the toast to hide.</param>
45+
public void Hide(ToastNotification notification)
46+
{
47+
#if WIN32
48+
PreprocessToast(notification);
49+
#endif
50+
51+
_notifier.Hide(notification);
52+
}
53+
54+
/// <summary>
55+
/// Adds a ScheduledToastNotification for later display by Windows.
56+
/// </summary>
57+
/// <param name="scheduledToast">The scheduled toast notification, which includes its content and timing instructions.</param>
58+
public void AddToSchedule(ScheduledToastNotification scheduledToast)
59+
{
60+
#if WIN32
61+
ToastNotificationManagerCompat.PreRegisterIdentityLessApp();
62+
63+
PreprocessScheduledToast(scheduledToast);
64+
#endif
65+
66+
_notifier.AddToSchedule(scheduledToast);
67+
}
68+
69+
/// <summary>
70+
/// Cancels the scheduled display of a specified ScheduledToastNotification.
71+
/// </summary>
72+
/// <param name="scheduledToast">The notification to remove from the schedule.</param>
73+
public void RemoveFromSchedule(ScheduledToastNotification scheduledToast)
74+
{
75+
#if WIN32
76+
PreprocessScheduledToast(scheduledToast);
77+
#endif
78+
79+
_notifier.RemoveFromSchedule(scheduledToast);
80+
}
81+
82+
/// <summary>
83+
/// Gets the collection of ScheduledToastNotification objects that this app has scheduled for display.
84+
/// </summary>
85+
/// <returns>The collection of scheduled toast notifications that the app bound to this notifier has scheduled for timed display.</returns>
86+
public IReadOnlyList<ScheduledToastNotification> GetScheduledToastNotifications()
87+
{
88+
return _notifier.GetScheduledToastNotifications();
89+
}
90+
91+
/// <summary>
92+
/// Updates the existing toast notification that has the specified tag and belongs to the specified notification group.
93+
/// </summary>
94+
/// <param name="data">An object that contains the updated info.</param>
95+
/// <param name="tag">The identifier of the toast notification to update.</param>
96+
/// <param name="group">The ID of the ToastCollection that contains the notification.</param>
97+
/// <returns>A value that indicates the result of the update (failure, success, etc).</returns>
98+
public NotificationUpdateResult Update(NotificationData data, string tag, string group)
99+
{
100+
return _notifier.Update(data, tag, group);
101+
}
102+
103+
/// <summary>
104+
/// Updates the existing toast notification that has the specified tag.
105+
/// </summary>
106+
/// <param name="data">An object that contains the updated info.</param>
107+
/// <param name="tag">The identifier of the toast notification to update.</param>
108+
/// <returns>A value that indicates the result of the update (failure, success, etc).</returns>
109+
public NotificationUpdateResult Update(NotificationData data, string tag)
110+
{
111+
#if WIN32
112+
// For apps that don't have identity...
113+
if (!DesktopBridgeHelpers.HasIdentity())
114+
{
115+
// If group isn't specified, we have to add a group since otherwise can't remove without a group
116+
return Update(data, tag, ToastNotificationManagerCompat.DEFAULT_GROUP);
117+
}
118+
#endif
119+
120+
return _notifier.Update(data, tag);
121+
}
122+
123+
/// <summary>
124+
/// Gets a value that tells you whether there is an app, user, or system block that prevents the display of a toast notification.
125+
/// </summary>
126+
public NotificationSetting Setting
127+
{
128+
get
129+
{
130+
#if WIN32
131+
// Just like scheduled notifications, apps need to have sent a notification
132+
// before checking the setting value works
133+
ToastNotificationManagerCompat.PreRegisterIdentityLessApp();
134+
#endif
135+
136+
return _notifier.Setting;
137+
}
138+
}
139+
140+
#if WIN32
141+
private void PreprocessToast(ToastNotification notification)
142+
{
143+
// For apps that don't have identity...
144+
if (!DesktopBridgeHelpers.HasIdentity())
145+
{
146+
// If tag is specified
147+
if (!string.IsNullOrEmpty(notification.Tag))
148+
{
149+
// If group isn't specified, we have to add a group since otherwise can't remove without a group
150+
notification.Group = ToastNotificationManagerCompat.DEFAULT_GROUP;
151+
}
152+
}
153+
}
154+
155+
private void PreprocessScheduledToast(ScheduledToastNotification notification)
156+
{
157+
// For apps that don't have identity...
158+
if (!DesktopBridgeHelpers.HasIdentity())
159+
{
160+
// If tag is specified
161+
if (!string.IsNullOrEmpty(notification.Tag))
162+
{
163+
// If group isn't specified, we have to add a group since otherwise can't remove without a group
164+
notification.Group = ToastNotificationManagerCompat.DEFAULT_GROUP;
165+
}
166+
}
167+
}
168+
#endif
169+
}
170+
}
171+
172+
#endif

0 commit comments

Comments
 (0)