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

Native notifications for Windows, Linux and OSX #3389

Closed
wants to merge 23 commits into from

Conversation

pr8x
Copy link
Contributor

@pr8x pr8x commented Dec 23, 2019

What does the pull request do?

Implementing native notification support for Windows (10), Linux (DBUS/FreeDesktop) and OSX.

linux_native_notifications


windows_native_notifications


osx_native_notification

Discussion

  • In Windows it is required to register an application shortcut in the StartMenu in order to send toast notifications (See https://docs.microsoft.com/en-us/windows/win32/shell/enable-desktop-toast-with-appusermodelid). I am not exactlly sure how we want to handle this. I could add a RegisterApplication function in the Win32Platform to do this registration and also create a unique AppUserModelID based on the Application.Name. It's probably a good idea to uninstall the shortcut once the application exits.

  • This PR provides a basic implementation that works across the three major desktop platforms. Advanced features (such as replacing notifications, custom buttons, images, etc) can be rolled in future updates.

What is the current behavior?

We only have managed notifications hosted inside avalonia windows.

What is the updated/expected behavior with this PR?

How was the solution implemented (if it's not obvious)?

  • Windows 10: Using the UWP ToastNotification API
  • Linux: Using the FreeDesktop DBUS org.freedesktop.Notifications service interface. (Thanks to [WIP] Native notifications #2884)
  • OSX: Creating a COM interface for interacting with the native Cocoa API (NSUserNotificationCenter)

Fixed issues

#2155

@@ -4,6 +4,9 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageId>Avalonia.Win32</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.18362.2005" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that work with Win 8.1 or is it Win10+ only?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Win10+ only from what I can tell.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows 10 WinRT API Pack enables you to add the latest Windows Runtime APIs support to your .NET Framework 4.5+ and .NET Core 3.0+ libraries and apps.

https://www.nuget.org/packages/Microsoft.Windows.SDK.Contracts/10.0.17763.1000-preview

But AFAIK the toast notifications only work in Windows 10 anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows 8.1 also has them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah Windows 8 and 8.1 both have support for the ToastNotification API. Not sure if we really care about Windows 8 though 😄 .

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want a managed fallback that displays notifications in separate windows manually. Like IM clients of the past did. That's not in the scope of this PR though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it's more of "do people who use Avalonia for their software care about Windows 8 and 8.1", not "if we care about it".

pr8x added 3 commits December 24, 2019 14:43
!F Close and action callbacks
!F Handling notification center events in seperate delegate
!T Renaming some files to be more consistent
@pr8x
Copy link
Contributor Author

pr8x commented Jan 4, 2020

@danwalmsley So how do we solve the problem with registering the application shortcut in the startmenu on Windows? Should I just register a temporary and uninstall it when the application terminates?

@MonkAlex
Copy link
Contributor

MonkAlex commented Jan 4, 2020

its not required, but will be used, if registered (not check, only google)
social.msdn.microsoft.com

@pr8x
Copy link
Contributor Author

pr8x commented Jan 4, 2020

In my experience deleting the shortcut (even when the AppUserModelID has been assigned to the current process via SetCurrentProcessExplicitAppUserModelID) will cause the toast nofications to be ignored by Windows. Most official Microsoft sources confirm this.

A shortcut to your app, with a System.AppUserModel.ID, must be installed to the Start screen. Note, however, that it does not need to be pinned to the Start screen

https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/hh802768(v=vs.85)?redirectedfrom=MSDN

Also it seems like once the shortcut was installed they use the name and the icon of it to display the application in the taskbar. Without a shortcut the taskbar icon will default to the main window icon AFAICT. So it might actually be a good idea to have some sort of RegisterApplication function on the Win32Platform that takes care of registering your AppUserModelID and assigning name and icon of your application via the shortcut.

# Conflicts:
#	src/Avalonia.Native/AvaloniaNativePlatform.cs
@pr8x pr8x marked this pull request as ready for review January 13, 2020 13:16
@pr8x pr8x changed the title [WIP] Native notifications for Windows, Linux and OSX Native notifications for Windows, Linux and OSX Jan 21, 2020
@grokys
Copy link
Member

grokys commented Jan 21, 2020

@danwalmsley this is ready for review now, when you have a moment.

samples/ControlCatalog/MainWindow.xaml Outdated Show resolved Hide resolved
samples/ControlCatalog/Pages/NotificationsPage.xaml Outdated Show resolved Hide resolved
@pr8x pr8x requested a review from danwalmsley February 1, 2020 13:19
Copy link
Member

@kekekeks kekekeks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to the comments: please, avoid doing formatting changes for the sake of doing formatted changes. It confuses git blame and makes it way harder to track the origin of a particular change. Same goes for renaming files. You'll probably have to squash your changes now.

src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj Outdated Show resolved Hide resolved

private async ValueTask Connect()
{
_isConnected = await
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the app might be started without a session bus or without org.freedesktop.Notifications service on the session bus. That's the case for VNC-powered sessions, WSL, embedded devices and other scenarios without a "full" desktop environment. We should gracefully fall back to some managed implementation in that case.

Use DBusHelper.Connection which will be just null if no session DBus is available like we are doing in our dbusmenu implementation. Then try to create a proxy and see if that throws. That check should be done before registering FreeDesktopNotificationManager in the service locator, so the rest of the code base won't be confused by dysfunctional notification manager. It would also be a good place to call GetCapabilitiesAsync.

Copy link
Contributor Author

@pr8x pr8x Feb 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also be a good place to call GetCapabilitiesAsync.

Which capabilties would you check for? At first glance the features we rely on should be working in all server implementations.

We should gracefully fall back to some managed implementation in that case.

You mean the WindowNotificationManager? Which Window should it be installed on though?

Copy link
Contributor Author

@pr8x pr8x Feb 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently we're only invoking DBusHelper.TryInitialize() when UseDBusMenu is enabled. We should probably be doing this unconditionally now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which Window should it be installed on though

The initial idea was that user code is supposed to get the notification manager from the window itself rather than using service locator. A single app might be using toplevels represented by different windowing systems. I. e. it's planned to have a "remote" mode for devtools when they are using our previewer remote widget infrastructure to render themselves to a remote machine (i. e. you will be able to connect to devtools of an application running on your android phone). So the general policy is to remove global variables when possible.

So there shouldn't be a globally accessible instance of the notification manager, only one accessed via ITopLevelImpl in some manner.

Copy link
Contributor Author

@pr8x pr8x Feb 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there shouldn't be a globally accessible instance of the notification manager, only one accessed via ITopLevelImpl in some manner.

Allright, but that seems vastly out of scope for this PR. For now the service will just return null when it fails to initialize. I guess the users can account for that in their code and fire up the WindowNotificationManager instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait... So instead of registering the notification manager (on all platforms) through AvaloniaLocator I should just make it a property on the TopLevel?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, probably an attached one. The general idea is that services should be resolved via toplevels (somewhat like in Android where every damn thing needs a Context)

Copy link
Member

@kekekeks kekekeks Feb 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, note how it works with global menu - toplevel impl implements an extra interface where it announces that it could potentially provide some service.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toplevel impl implements an extra interface

I can't seem to find the interface you're referring to. What is it called?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl

_notifications = new Dictionary<int, INotification>();
_notificationCloseCallback = NotificationOnClose;
_notificationActionCallback = NotificationOnAction;
_native.CloseCallback = Marshal.GetFunctionPointerForDelegate(_notificationCloseCallback);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use function pointers with Avalonia.Native. Create a COM interface and implement it on the C# side instead. We are automatically generating native-to-managed call proxies for interfaces with Events suffix.

The preferred way would be to not use an int identifier field in AvnNotification but introduce

AVNCOM(IAvnNotificationEvents, 21) : IUnknown
{
    virtual void OnAction() = 0;
    virtual void OnClose() = 0;
};

and create a new instance of a class implementing that interface for each ShowNotification call and pass it along with other notification info, then keep it in ComPtr<IAvnNotificationEvents> on the native side. That way the callback life time would be managed automatically and we won't need to keep the GC-rooted dictionary with all of the notifications.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preferred way would be to not use an int identifier field in AvnNotification

I guess once we implement replacing of notifications it still makes sense to have some id field to identify the OSX notification we want to replace.

then keep it in ComPtr on the native side

That means I have to store a vector of all the ComPtr<IAvnNotificationEvents> and release them when the notification closes? AFAIU you want the interface of IAvnNotificationManager to be ShowNotification(AvnNotification* notification, IAvnNotificationCallbacks* callbacks), correct?

Copy link
Member

@kekekeks kekekeks Feb 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need a vector?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user can show multiple notifications. I need to store their corresponding callbacks somewhere in order to call them from the global NotificationCenterDelegate. I could plug them into the NSUserNotification.userInfo somehow, I suppose. userInfo expects a map of id, though (which is essentially a pointer to objc_object). So I guess I need to find some ugly workaround of putting a ComPtr in there...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userInfo expects a map of id

it seems that OSX expects the contents of userInfo to be serializable. I'm afraid that might need to change the API design to accomodate OSX notifications properly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What API design are you thinking of? For now, we could also keep it with the Dictionary on C# and the function pointers. And revisit it in the future once we figured out a good/better way of designing this API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, we could also keep it with the Dictionary on C# and the function pointers.

That can and will lead to memory leaks.

src/Windows/Avalonia.Win32/Win32Platform.cs Outdated Show resolved Hide resolved
- (NSString *)__bundleIdentifier;
{
if (self == [NSBundle mainBundle]) {
return @"com.avalonia.native.osx";
Copy link
Member

@kekekeks kekekeks Feb 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add an option to configure that with AvaloniaNativeMacOptions and fall back to "com.avaloniaui.apps."+Assembly.GetEntryAssembly().Name

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably should also somehow interact with bundle identifier from Info.plist if the app is running from an actual app bundle.

Copy link
Contributor Author

@pr8x pr8x Feb 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we should check the [[NSBundle mainBundle] bundleIdentifier] and only install the fake bundle identifier when there is no mainBundle identifier.

@kekekeks
Copy link
Member

kekekeks commented Feb 1, 2020

@pr8x it seems that OSX notifications kinda "live forever" and one is expected to track them using a string identifier and up to 1KB of data in userInfo. It seems that the current API is simply not suitable for dealing with OSX notifications and might need to be redesigned completely. Not sure if that's in the scope of this PR though.

With the current API we might want to disallow using arbitrary huge expiration times, so we could assume that notification will always expire in a reasonable time.

@pr8x
Copy link
Contributor Author

pr8x commented Feb 1, 2020

@kekekeks What do you mean by "live forever"? They just stay in the notification center until the user manually removes them? I guess we can default to some reasonable expiration time when the user doens't specify it. But what is the problem with them just staying in the notfication center?

@kekekeks
Copy link
Member

kekekeks commented Feb 1, 2020

Do they stay in the notification center after application is closed?

@pr8x pr8x force-pushed the native_notifications_win_linux_osx branch from ad46e01 to 7323816 Compare February 16, 2020 13:12
@pr8x
Copy link
Contributor Author

pr8x commented Feb 16, 2020

@kekekeks Yes, the notifications indeed stay when the application exits. I should probably listen to some application shutdown event and call removeAllDeliveredNotifications()

@kekekeks
Copy link
Member

We should also call removeAllDeliveredNotifications on startup, I think.

@kekekeks
Copy link
Member

BTW, same happens on KDE.

@danwalmsley
Copy link
Member

closing due to inactivity... @pr8x if you would like to resolve the conflicts we can resurrect this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants