Open yusuf8ahmed opened 4 years ago
@yusuf8ahmed Absolutely! We'd love to have an API for notification popups (often called "Toasts", because they pop-up like a piece of toast from a toaster) built into Toga!
@danyeaw wrote up a blog post about the process of developing a new widget for BeeWare - he gave that content as a talk at PyCon US 2019. "Toast notifications" aren't strictly widgets, but the process will be very much the same.
The first step is some research. It looks like you know how to do this in Windows; however, we need to make sure that the Toga API you propose is usable on other platforms. You don't have to write the implementation for macOS, Linux, Android and iOS - but we need to have some idea of the concepts that a generic API needs to cover. Are there platform limitations we need to be aware of? How long can push messages be? Do we have to provide title and description? Do we need to provide an icon? Are there any other features we may want to control programatically? Are there any OS-level registration requirements?
Once you've got that research, you can propose an API. I'm guessing this will be an app-level API - i.e., an entry point on the App object - but if some other API makes sense to you, feel free to propose that.
Thanks for the quick response. I am like 2 hours into basic research and all proposals are tentative and can change.
I currently have a Toaster API that can work on Windows, macOS and Linux. These are all os native toasts.
Windows (currently complete and in testing phase): Using Subprocess.call() to call a PowerShell script with functions assigned to every types of notification
https://docs.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype?view=winrt-18362
macOS (Build and then testing): Using Subprocess.call() to run osascript(Applescript) current only a title and subtitle Yikes! I am looking to use PyObjc or Objective-c as a type a bridge to access the native toasts
https://pyobjc.readthedocs.io/en/latest/api/module-objc.html https://stackoverflow.com/questions/17651017/python-post-osx-notification/21534503#21534503 https://blog.gaelfoppolo.com/user-notifications-in-macos-66c25ed5c692
Linux (Build and then testing):
For Linux I am specifically targeting Ubuntu for full toast Compatibility. I am currently under the impression that the command _notifysend work universally
https://manpages.ubuntu.com/manpages/xenial/man1/notify-send.1.html
ios and android (Look for a place to start): I don't know how to start this part. Let me know if you place that I start brainstorming on way to execute this part of the API.
Answers
Are there platform limitations we need to be aware of? -Currently, only ios and android as I am confused on how to start
How long can push messages be? -If the native os has no limit then it can be as long as you want.
Do we have to provide a title and description? Usually you would but in windows, you can just send a single string to toast
Do we need to provide an icon? -You can if you want to.
Are there any other features we may want to control programmatically? -As of right now, I am making it that it is just a toast message with no extra functionality
Are there any OS-level registration requirements? -All must have Python 3.6+
-Windows users must have Powershell and .NET work correctly and -Linux(Ubuntu) users must have notify-send package working correctly and Python 3.6+ -macOS user must be 10.8+ (Mountian Lion to Catalina)
Please message back as I would like to have some feedback on the current plan
@yusuf8ahmed Thanks for doing that research - definitely a good start!
Calling into shell commands isn't really an option here. We have access to the full system APIs, so we should be using those unless there is absolutely no other option.
On macOS specifically, we use a library called Rubicon to invoke any Objective C calls that are needed. Rubicon and PyObjC are similar in terms of API, so the Stack Overflow examples you've presented should be portable without much effort.
On Linux, the solution definitely won't be a command line tool - it will be something that integrates with the FreeDesktop.org D-BUS specification. The notify-send
tool that you've referenced is a tool that is compliant with that specification, but there will be other ways (and almost certainly a Python API of some kind) to inject compliant messages.
I suspect that investigating those APIs further will also reveal some gaps in your capabilities research. For example: Take the code snippet from the Stack Overflow macOS example:
notification.setTitle_(str(title))
notification.setSubtitle_(str(subtitle))
notification.setInformativeText_(str(text))
notification.setSoundName_("NSUserNotificationDefaultSoundName")
notification.setHasActionButton_(True)
notification.setActionButtonTitle_("View")
notification.setUserInfo_({"action":"open_url", "value":url})
That indicates that on macOS, you have the option for:
and the app can respond to that action. An icon isn't part of that example; but looking at docs, there's support for a contentImage
, as well as multiple other action types, and control for the delivery time.
(I'll also note that the Apple documentation suggests that NSUserNotification has been deprecated in favor of the UserNotifications framework, which appears to also work on iOS; however, it doesn't work on macOS before 10.14).
So - which of those APIs are required? What in-practice limits exist to the length of text labels? The size of images?
It's entirely understandable that you don't want to try and build everything at once - that's definitely a good idea; but we need to be sure that what we build now won't paint us into a corner later on. We need to be certain about what is absolutely required and what is optional. And If something is required on one platform but not another, what is our cross-platform story?
Lastly - the OS limits you mention aren't a huge problem; but you mention Python 3.6 as a minimum version. Is there a technical reason for that limitation? Toga still supports Python 3.5; what feature/library necessary for this project isn't supported on Python 3.5?
@freakboy3742 Thank you for the quick reply. This the current API layout/solution.
Windows (testing phase): Currently, I have a dll file from C# and it is basically a line for line copy of PowerShell scripts Code Here
macOS (Building): I have a full-featured 10.8-10.14 toast notifier in Pyobjc but I am currently having some trouble accessing apple frameworks within rubicon-objc. If you can send some links or info that would be very amazing
Linux (Building): Currently on the back burner but I found python-dbus and found some useful examples on pydbus
Currently, I am looking to build the project in parts as I have the whole summer and not to rush to push a defective code
In the previous comment, I said python 3.6+ would be the OS-level registration requirement. As I want people to be on a stable version but technically this current solution should work with 3+.
@yusuf8ahmed Before you start down a path of getting a C# implementation working for Windows, you may want to take a moment and look a the existing source of Toga. There's not a single line of C# in that codebase - it's all Python. We're using Python.net to bridge between Python and the .NET runtime. There shouldn't be any need for a C# codebase; and I'm unlikely to merge a patch that introduces one.
As for Rubicon - I'm not sure what pointers you're looking for. Rubicon itself has fairly extensive documentation; but if you've got working PyObjC code, it should be a relatively straightforward transformation to get Rubicon working. You'll need to elaborate on the problems you're seeing or the conversion you don't understand.
Pydbus might be a viable option on Linux - however it certainly wouldn't be my first choice. Firstly, it's a little concerning that it hasn't been updated in over 2 years. A 5 second Google search for "dbus python" revealed dbus-python, which describes itself as "the original Python binding for DBus", and has published versions as recently as January of this year. dbus-python also mentions GDBus as another option accessible through PyGI, which we already use. So - more research may be needed here.
Also - before you get too deeply involved in implementations, you still need to present a design. There are still open questions about what features a "toast" API needs to have.
@freakboy3742 Thank you for the quick response,
I would be glad to present a design. Can you give me a quick overview of what you expect to see in it?
C# is not included, there is only one .dll file. I specifically chose the dll file route as there is no other stable way to access the native toast API of Windows. I have tried and looked at many different ways such as: -Microsoft xlang/winRT for python: Not actively maintained, Poor documentation and faulty code. -pywin32: no access to winRT API which is need for notifications.
A dll is actually the most efficient to use as toga already uses Python for .NET which is need to load in the dll.
To explain my rubicon-objc problem further I am having a problem accessing Apple App Service called UserNotifications specifically
For Linux I 100% agree with you pydbus is not an option and that dbus-python is a way better option. Also are you able to send me more information on PyGI and GDBus.
@freakboy3742 Thank you for the quick response,
I would be glad to present a design. Can you give me a quick overview of what you expect to see in it?
A proposal for an API. What methods will exist in the API? How will end-users access the API?
And - you can't answer that question without addressing the original research question that I posed: what are the that a generic API needs to cover? You did some initial research, and I pointed out some gaps in that research - but you haven't addressed any of those gaps.
C# is not included, there is only one .dll file.
Yes, which is one more DLL than is currently shipped with the entire project. Do not underestimate the inherent complexity involved in "just" adding a DLL.
I specifically chose the dll file route as there is no other stable way to access the native toast API of Windows.
This is patently untrue. 5 minutes of Google research for "winforms notification API" lead me to the page on NotifyIcon.ShowBalloonTip; 5 minutes after that, I had the following Python code:
notifyIcon1 = WinForms.NotifyIcon()
notifyIcon1.Icon = SystemIcons.Exclamation
notifyIcon1.BalloonTipTitle = "Balloon Tip Title"
notifyIcon1.BalloonTipText = "Balloon Tip Text."
notifyIcon1.Visible = True
notifyIcon1.ShowBalloonTip(3000)
So no - a DLL is not the only option.
To explain my rubicon-objc problem further I am having a problem accessing Apple App Service called UserNotifications specifically
That... is not an explanation of anything. It's telling me the name of an Objective C class. It's not telling me:
or anything else that would allow me to help you.
For Linux I 100% agree with you pydbus is not an option and that dbus-python is a way better option. Also are you able to send me more information on PyGI and GDBus.
I can't provide you with anything that can't be provided by Google.
@yusuf8ahmed, if you would like to pointers on backend implementation for macOS and Linux, you can have a look at https://github.com/SamSchott/maestral/blob/master/maestral/utils/notify.py.
Linux: This uses the dbus specification on Linux (through jeepney). python-gobject could be an alternative since toga already depends on it.
macOS: This uses UNUserNotificationCenter on macOS (should also work on iOS) with a fallback to NSUserNotificationCenter on older releases.
There is currently no implementation of buttons and callbacks when a notification is clicked, both would likely need some integration with an event loop.
Of course, the API is very different from the toga APIs. But the backend should be helpful.
Thank you very much
Just for reference, I've recently recently released a Python library desktop_notifier for cross-platform desktop notifications, currently for Linux and macOS only.
Features are:
The library is purely written in Python, with Python-only dependencies: it uses rubicon-objc on macOS and dbus-next on Linux. With toga, we could of course use python-gobject instead. Asyncio / event loop integration is used to react to user interaction with the notification.
Regarding the API, it currently exposes only a single method, but this may be expanded in the future:
from desktop_notifier import DesktopNotifier, NotificationLevel
notifier = DesktopNotifier(app_name="Sample App", notification_limit=10)
notifier.send(
title="Hello from Python!",
message="A horrible exception occured",
urgency=NotificationLevel.Critical,
icon="/path/to/icon.png",
action=lambda: print("notification clicked"),
buttons={
"Button 1": lambda: print("Button 1 clicked"),
"Button 2": lambda: print("Button 2 clicked"),
},
)
For a toga API, I can see multiple options:
App.notify()
similar to the above API. This may either raise errors or return a boolean indicating if showing the notification succeeded. There can be multiple reasons why it would fail: The end user didn't give authorisation for notifications, there is no DBus service or desktop environment to send notifications, etc. Raising an error will allow us to convey the failure reason while returning False will reflect that notifications not appearing may be quite common and not an "exception" level event.App.notification_center
with multiple attributes such as:
send()
: Send a notification.request_authorisation()
: Explicitly ask for authorisation to send notifications instead of letting toga ask on-demand or on init. This lets the user control the timing of possible prompts from the OS.has_backend
: Bool indicating if we could connect to a backend such as a Dbus service or UNUnserNotificationCenter (not available on some CI runners and headless sessions).has_authorisation
: Bool indicating if we are authorised.What do people think regarding the API? If there is some consensus, I'd be happy to work on a PR providing the core API and Linux + macOS implementations.
I can contribute notifications for Android: https://www.tanapro.ch/products/taTogaLib/docs/html/system/notifications.html
@freakboy3742 Where would we place the notification code in Toga? I'm thinking of a notification module in the system package, e.g. from toga.system.notification import Notification, NotificationManager
How should we handle the icon? I think, it should be relatively easy to provide a default icon for every platform, probably the "info" icon or maybe (if I can find out how) the application icon of the beeware app. But it might be quite difficult to let the user choose an icon, because finally, we need to provide the platform-specific icon, but in the interface, it should be something generic, like a Toga Icon. For desktops, it might be possible to convert the Toga Icon into the platform specific icon, but on Android, the icon should be compiled and Android only wants the resource reference (integer) to it.
@freakboy3742 Where would we place the notification code in Toga? I'm thinking of a notification module in the system package, e.g. from toga.system.notification import Notification, NotificationManager
So - there isn't a system package at present, and I'm not sure what else you're anticipating being a "system" capability - but this seems (a) a much deeper package hierarchy than is needed (toga.notifications
would seem more than sufficient if we went down this path), and (b) problematic because it doesn't inherently have a reference to the app.
I haven't done a whole lot of thinking about this beyond "this is something we need", so I can't say I have a well-thought-out API design in mind. @samschott's comment seems like a good starting point; my inclination is that we'd probably want to go with option (2) (or a variation similar to it - app.notifications
would seem an easier name than app.notification_center
) to preserve a namespace for permission checks and other notification-related functionality.
How should we handle the icon? I think, it should be relatively easy to provide a default icon for every platform, probably the "info" icon or maybe (if I can find out how) the application icon of the beeware app.
... is there something wrong with toga.App.icon
?
But it might be quite difficult to let the user choose an icon
... is there something wrong with toga.Icon
?
I'm not sure what else you're anticipating being a "system" capability
I'm thinking about clipboard access, power management (e.g. for keeping an Android app awake), permissions management.
is there something wrong with toga.App.icon?
For the Android notification, I need a resource ID (integer) to some compiled image. How do I get the resource ID of toga.App.icon?
toga.Icon
doesn't currently use the Android resource system, so you'd have to use a different approach.
In Java you'd access the app's icon as R.drawable.ic_launcher
, but the package of R depends on the application ID. So the simplest way to get the resource ID in Toga is probably something like this.
@mhsmith I tried to use R.drawable.ic_launcher as Icon reference, but got following error:
type object 'R$drawable' has no attribute 'ic_launcher'
In Resources.getIdentifier(), I need to specify package
and type
.
What should I pass as type
?
I tried to use R.drawable.ic_launcher as Icon reference, but got following error:
type object 'R$drawable' has no attribute 'ic_launcher'
This might work as long as you're using the R
class in the application's own package, e.g. my.application.id.R
. You can get the application ID from context.getApplicationInfo().packageName
.
In Resources.getIdentifier(), I need to specify
package
andtype
. What should I pass astype
?
"drawable"
@mhsmith I tried following code (using my tatogalib library):
mgr = NotificationManager()
ctx = mgr._impl.context
res = ctx.getResources()
pkg = ctx.getApplicationInfo().packageName
icon = res.getIdentifier("ic_launcher", "drawable", pkg)
self.fnPrintln(f"package: {pkg}")
self.fnPrintln(f"icon: {icon}")
noti = Notification(
"My title", text, icon
)
id = mgr.post_notification(noti)
pkg
is correctly returned as ch.tanapro.pyplayground
, but icon
is 0 which means that the resource could not be found :-(
The post_notification
then raises an exception "Invalid notification (no valid small icon)"
hmm...but this works:
icon = res.getIdentifier("abc_ic_search_api_material", "drawable", pkg)
also this works:
from ch.tanapro.pyplayground import R
icon = R.drawable.abc_ic_search_api_material
It seems that there really is no "ic_launcher" icon in the app, although I built it with a pretty recent briefcase (0.3.12).
In pyprojct.toml, I have:
icon = "src/pyplayground/resources/pyplayground"
and
icon.round = "src/pyplayground/resources/pyplayground-round"
icon.square = "src/pyplayground/resources/pyplayground-square"
build_gradle_extra_content = "android.defaultConfig.python.extractPackages 'pyplayground.resources'"
@mhsmith
It works with icon = res.getIdentifier("ic_launcher", "mipmap", pkg)
, not with drawable
:-)
And with a full resource name, it also works: icon = res.getIdentifier(f"{pkg}:mipmap/ic_launcher", None, None)
So, we could use the application icon as default icon and let the user choose a custom icon using a string. On Android, this would be a full resource name and on Windows, it could be a path to some icon file
Starting with Android 7.0, it is possible to use Icons built from a file as the notification's small icon. So, the icon parameter could be a file path for both, Windows and Android
OK, Android 7.0 is BeeWare's minimum supported version anyway, and we want to encourage the programmer to write portable code, so how about this:
I'd suggest something slightly different (or, at least, in addition to this). Loading from file/data (same as Image) should be legal; but there's a third option of "system icon identifier". These are constants, and cover icons that are commonly provided by the system - the app icon, plus other utility icons like error, warning, info etc.
These icons can be exposed as Icon.APP (although we might want to stick with toga.App.icon, rather than Icon.APP), Icon.ERROR, Icon.WARNING etc. Platforms can then choose how to satisfy those icon requirements - if Android needs to use a resource identifier, then that's an implementation detail, and only needs to be exposed in the backend API, not the public API.
there's a third option of "system icon identifier"
Yes, I like this idea. What data type would Icon.ERROR be? An Integer? Then, we could differentiate the three cases based on their type: None, int, str
I implemented Russel's suggestions regarding the icons in my tatogalib (only for Winforms, Android will follow): https://www.tanapro.ch/products/taTogaLib/docs/html/system/notifications.html
I would appreciate your comments on this implementation, so I can take those into account for my toga PR. In the PR, I will use the Icon class for the system icons, not the separate AppIcon class.
Hmm...I just realized that AppIcon.ERROR is missing in the docs. I'll fix that soon.
there's a third option of "system icon identifier"
Yes, I like this idea. What data type would Icon.ERROR be? An Integer? Then, we could differentiate the three cases based on their type: None, int, str
Why wouldn't it be Icon
? There's no need to expose the constant itself to the end user. There's already Icon.TOGA_ICON
and Icon.DEFAULT_ICON
; all we're talking about changing is an implementation detail for how some of those icons will be constructed.
I would appreciate your comments on this implementation, so I can take those into account for my toga PR.
So - I'm only going off the documentation here, but:
The API naming seems... verbose. tatogalib.system.notifications.NotificationManager().post_notification(Notification("My title", text, AppIcon.INFO))
... why not tatogalib.notifications.post("My Title", text, AppIcon.INFO)
? If you're on an instance of a notification manager, you don't need to include "notification" in every API endpoint.
In particular, why is there a Notification
class, when all the APIs after posting a notification use the notification ID? What use is the wrapper object?
I'm unclear what's going on with the fnLog
/log
handler, or why this functionality is needed.
It's also not clear why a NotificationManager class is needed; at an implementation layer, it might make sense, but as an exposed Python API this is something that should be handled with a singleton or Borg pattern, or something that is encompassed by attribute access.
Lastly - in terms of the path forward into Toga itself; I'm not clear what you have in mind, but I'm unlikely to approve a PR that depends on taTogaLib
as a dependency. Adding any dependency requires a high bar, as it's code that we're not maintaining ourselves.
I've just been reminded of #1890 - I'm not sure that discussion adds much more than what is here, but it's some more context.
@freakboy3742 Thanks for your comments. I always learn a lot from your feedbacks.
Of course I do not plan to create toga dependencies with tatogalib. My tatogalib library is meant for all the stuff that does not exist in toga.
The fnLog / log is for debugging during development only. I will remove this in the PR.
If I correctly understand you, we could implement the notifications similar to self.app.paths, right? So, we would then have a self.app.notifications member which would be an instance of NotificationManager (which we might still rename to something simpler)
Yes - A notifications
object analogous to paths
would be the sort of thing I had in mind. However unlike Paths, I'm guessing at least part of the notification infrastructure won't be instantiated until actual use. Paths is mostly a collection of constants, so we can create the Paths() instance when the App instance is created at essentially zero cost. AIUI, this won't be true of NotificationManager (or whatever we end up with) - there's native objects to create that the user won't need unless they're actually doing notifications. Whether that means the underlying NotificationManger-analog isn't instantiated until first access of app.notifications
, or there's some internal state in the NotificationManager that isn't fully configured until some attempt to use notifications is made, I don't know.
Part of the current __init__
is the creation of the notification channel. On Android, this does not need to be done for apps that do not want to use notifications. On the other hand, for apps that do want to use notifications, the docs say this should be done as early as possible, because the permissions can only be given by the user when the channel exists.
As for the permissions: I have not yet found out how to prompt the user for acceptance from within the app. So far, I started the app to have the channel created and then gave the permission "manually" in the app's Android settings. If the user is prompted from within the app, this should only be done once, because when he refuses, he doesn't want to be asked on every app start again. So, the app should persist whether it already asked the user which is something we don't want handled by toga.
Maybe, we should use an explicit app.notifications.initialize()
method that an app must call on startup when it wants to use notifications. This method would then create the native objects.
Or, app.notifications
is normally None and importing the toga.notifications
package in the application would initialize the app.notifications
. This is how I currently do it in the latest version of tatogalib:
https://www.tanapro.ch/products/taTogaLib/docs/html/system/notifications.html
Or, we go back to my original idea where the NotificationManager must explicitly be instanciated by the app if it wants to use notifications...
Hmm...this is strange: The Windows notifications are displayed for a while and then, they disappear automatically. I would expect that I can still see them in the notification center, but this is not the case. At least, not anymore. They did stay when I still had the ExplorerPatcher for win11 installed. Now, after uninstalling ExplorerPatcher (and installing SystemTrayMenu), the notifications are shown, disappear automatically and are NOT in the notification center Does anyone know how to make them stay in the Windows 11 notification center?
Most of the discussion above is related to Windows notification. May I know what is the best way to get, in a Toga generated App on Android (and later I would need also for MacOS), notifications in the mobile phone sent from a server and received by the phone ?
Thanks
At present, any implementation of notifications will be entirely platform-specific, invoking the platform-specific APIs to manifest a notification.
I've played around with toga a little bit and it is amazing!
I'm building a personal app and I wanted the ability to create custom push notifications. I have the whole system built but I think this feature would be very beneficial for the broader toga community
I am willing to personally work on the PR for this feature. Can I get some guidance or advice on how to proceed? Appreciate it!