kivy / plyer

Plyer is a platform-independent Python wrapper for platform-dependent APIs
https://plyer.readthedocs.io
MIT License
1.63k stars 425 forks source link

Push Notification issue #740

Open KocWozniakPiotr opened 1 year ago

KocWozniakPiotr commented 1 year ago

It seems plyer notification module works finally out of the box on Android. However I'm having a hard time getting push notifications to work inside my service.pyscript which runs in foreground.

When I start some notification from inside of main app notify()works fine. Also when a foreground service.py runs alongside. But it fails when I implement notify()inside my foreground service. No matter if the app stays active, paused or I close it. The same happens.

The main Service Notification stays sticky as it should and when notify() tries to generate a new push notification Service Notification dissapears for a second and appears back. However with no Title and content anymore. Just blank notification.

here is my logcat when background service tries to use plyer push_notification:

01-26 13:09:08.584 2275 2275 D SystemUILog: notification|NotificationListener:onNotificationPosted: StatusBarNotification(pkg=org.kivy.sampleapp user=UserHandle{0} id=1 tag=null key=0|org.kivy.sampleapp|1|null|10615: Notification(channel=org.kivy.p4a1 shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE))

In my main.py I'm using the usual service.start()

from jnius import autoclass service = autoclass('org.kivy.sampleapp.ServiceSyncing') mActivity = autoclass('org.kivy.android.PythonActivity').mActivity service.start(mActivity, '')

service.py

from jnius import autoclass from plyer import notification

PythonService = autoclass('org.kivy.android.PythonService') PythonService.mService.setAutoRestartService(True)

while True: sleep(60) notification.notify(title='Hey', message='testing')

buildozer.spec attached as a file

buildozer.txt

I'm kind of lost. I think it might have something to do with id or channel_id getting changed or because I'm not using the main package/activity ? I'm not very familiar with java code so I cant figure out how and where to alter the code. Before 2.1.0 I was able to run it successfully.

Any help or workaround for this would be great !

KocWozniakPiotr commented 1 year ago

I solved the problem only partially by bumping my min.ndk version to my min.sdk version in buildozer.spec . In my case it's 26.

Still the problem remains while Notification system tries to access icon when displaying push notifications from background activity rather than the main app itself. I'm not very familiar with java but in notification.py it appears self._app_icon = info.icon doesn't actually have the access to the original app icon anymore when trying to retrieve info from the current package_name. For now I need to go back again to the "presplash" workaround using Drawable to get it back to work.

It would be nice to address it in the future version and to be able to change the icon and id freely on the go by adding arguments in the notify() itself. It would make the whole thing more flexible and easier to customize different types of notifications.

For instance:

notify(title, message, custom_notification_icon, id)

misl6 commented 1 year ago

Possibly duplicate of #748 ?

@KocWozniakPiotr can you share the full output?

KocWozniakPiotr commented 1 year ago

Not really. This is not about the background service itself but more about Notification Class not flexible enough.

Ideally you would preferably want to send notifications from a background service and not app itself. The problem here is lack of parameter different_id inside notify() .

This leads to the conflict between notification having same ID as background service.

I did modify the notification class to have a different ID - avoiding the ID of the background service and now it works without any problem. As I already mentioned in a previous comment - it would be great if notify() method had more customization parameters to send a particular message to particular channel using different ID. Otherwise it has no practical usage in an actual app.

Since the latest buildozer introduced custom icon_path for later usage as a Drawable , I no longer have problems displaying (custom) icons inside notifications.

SametYanik commented 1 year ago

Tam olarak değil. Bu, arka plan hizmetinin kendisiyle ilgili değil, daha çok Bildirim Sınıfının yeterince esnek olmamasıyla ilgili.

İdeal olarak, tercihen uygulamanın kendisinden değil, bir arka plan hizmetinden bildirim göndermek istersiniz. Buradaki sorun, notify() içindeki parametre eksikliğidir different_id .

Bu, arka plan hizmetiyle aynı kimliğe sahip bildirim arasında çelişkiye yol açar.

Bildirim sınıfını farklı bir kimliğe sahip olacak şekilde değiştirdim - arka plan hizmetinin kimliğinden kaçındım ve şimdi sorunsuz çalışıyor. Daha önceki bir yorumda belirttiğim gibi, notify() yönteminin belirli bir mesajı farklı kimlik kullanarak belirli bir kanala göndermek için daha fazla özelleştirme parametresi olması harika olurdu. Aksi takdirde, gerçek bir uygulamada pratik bir kullanımı yoktur.

En son buildozer, daha sonra kullanım için özel icon_path'i bir olarak sunduğundan Drawable, bildirimlerin içinde (özel) simgeleri görüntülemekte artık sorun yaşamıyorum.

Hello. I'm having trouble getting notifications from background service. I'm getting errors. Any chance you could provide a simple example including main.py , service.py and buildozer.spec ? I think I did everything right, but every time I get an error.

irolokirt commented 1 year ago

Hi. Same problem here:

Notifications works from the main app using:

main.py

import plyer
...
plyer.notification.notify(title = "Hello", message = "World!")

From the service i've tested the following cases:

service_01.py

import plyer
...
plyer.notification.notify(title = "Hello", message = "World!")
from plyer import notification
...
notification.notify(title = "NOTIFY from Service", message = "Hello!")

in boot cases i get NotImplementedError: No usable implementation found! and AttributeError: 'org.kivy.android.PythonService' object has no attribute 'getComponentName'`

04-01 11:06:21.966 18450 18469 I Autoupdate: Service_01 running... 1
04-01 11:06:26.973 18450 18469 I Autoupdate: Service_01 running... 2
04-01 11:06:31.980 18450 18469 I Autoupdate: Service_01 running... 3
04-01 11:06:36.984 18450 18469 I Autoupdate: Service_01 running... 4
04-01 11:06:41.990 18450 18469 I Autoupdate: Service_01 running... 5
04-01 11:06:43.156 18450 18469 I Autoupdate:  Traceback (most recent call last):
04-01 11:06:43.156 18450 18469 I Autoupdate:    File "/home/zinig_ub/KIVY_WS/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/v01/arm64-v8a/plyer/utils.py", line 97, in _ensure_obj
04-01 11:06:43.157 18450 18469 I Autoupdate:    File "/home/zinig_ub/KIVY_WS/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/v01/arm64-v8a/plyer/platforms/android/notification.py", line 201, in instance
04-01 11:06:43.157 18450 18469 I Autoupdate:    File "/home/zinig_ub/KIVY_WS/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/v01/arm64-v8a/plyer/platforms/android/notification.py", line 48, in __init__
04-01 11:06:43.157 18450 18469 I Autoupdate:  AttributeError: 'org.kivy.android.PythonService' object has no attribute 'getComponentName'
04-01 11:06:43.157 18450 18469 I Autoupdate:  Traceback (most recent call last):
04-01 11:06:43.157 18450 18469 I Autoupdate:    File "/home/zinig_ub/KIVY_WS/.buildozer/android/app/service_01.py", line 22, in <module>
04-01 11:06:43.158 18450 18469 I Autoupdate:    File "/home/zinig_ub/KIVY_WS/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/v01/arm64-v8a/plyer/facades/notification.py", line 84, in notify
04-01 11:06:43.158 18450 18469 I Autoupdate:    File "/home/zinig_ub/KIVY_WS/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/v01/arm64-v8a/plyer/facades/notification.py", line 93, in _notify
04-01 11:06:43.158 18450 18469 I Autoupdate:  NotImplementedError: No usable implementation found!

Using:

from plyer.platforms.android import notification
PlyerNotification = notification.instance()
...
PlyerNotification.notify(title = "NOTIFY from Service", message = "Hello!")

I get error on PlyerNotification = notification.instance()

04-01 10:37:27.362 12967 12983 I Autoupdate:    File "/home/zinig_ub/KIVY_WS/.buildozer/android/app/service_01.py", line 6, in <module>
04-01 10:37:27.362 12967 12983 I Autoupdate:    File "/home/zinig_ub/KIVY_WS/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/v01/arm64-v8a/plyer/platforms/android/notification.py", line 201, in instance
04-01 10:37:27.362 12967 12983 I Autoupdate:    File "/home/zinig_ub/KIVY_WS/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/v01/arm64-v8a/plyer/platforms/android/notification.py", line 48, in __init__
04-01 10:37:27.363 12967 12983 I Autoupdate:  AttributeError: 'org.kivy.android.PythonService' object has no attribute 'getComponentName'

@KocWozniakPiotr please, can you share the modification you did to the notification class?

i've found out that in PythonService.java the foreground service notification is created with it's own channel:

...
String NOTIFICATION_CHANNEL_ID = "org.kivy.p4a" + getServiceId();
String channelName = "Background Service" + getServiceId();
NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, 
    NotificationManager.IMPORTANCE_NONE);

chan.setLightColor(Color.BLUE);
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(chan);

Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID);
builder.setContentTitle(contentTitle);
builder.setContentText(contentText);
builder.setContentIntent(pIntent);
builder.setSmallIcon(smallIconId);
notification = builder.build();
}
startForeground(getServiceId(), notification);

while in class AndroidNotification(Notification): def __init__(self) the notification is created by referring to the main application activity ID

package_name = activity.getPackageName()
self._ns = None
self._channel_id = package_name
KocWozniakPiotr commented 1 year ago

Sorry guys to resurrect old issue. Apologize for not being active during the last 6 months, to be able to reply!

PythonService.java wasn't modified.

First of all, If you want the notification properly working inside the background service process like in my case:

msg_id = 3 notification.notify(chan=msg_id, title=f'channel {msg_id}', message=f'{msg_counter[msg_id]}x {msg_template[msg_id]}')

You can modify notification.py Notification class inside facades directory to add the chan argument like here:

    def notify(self,chan=0, title='', message='', app_name='', app_icon='',
               timeout=10, ticker='', toast=False):

If using only one channel is all you want, just use only single channel for instance 0 or 1. Otherwise feel free to add as many channels as you please.

notification.notify(chan=1, title=f'channel no_1', message="message for channel 1"')


regarding channel service inside notification.py in the path /platforms/android

You need to add the channel there and optionally add Drawable which is sadly not supported anymore AFAIK. This will allow you to use a custom notification icons for each new channel you want to use later on in your app, which is really nice. The removal of the Drawable = autoclass("{}.R$drawable".format(activity.getPackageName())) in the newer plyer versions was partially the reason why the notifications kept crashing on the screen.

There are also few other changes for instance: app_icon = Drawable.your_icon_name_without_extensionname and few other things I do'nt remember correctly. But you can compare it with the source file.

This is my notification.py :

from android import python_act
from android.runnable import run_on_ui_thread
from jnius import autoclass, cast

from plyer.facades import Notification
from plyer.platforms.android import activity, SDK_INT

AndroidString = autoclass('java.lang.String')
Context = autoclass('android.content.Context')
NotificationBuilder = autoclass('android.app.Notification$Builder')
NotificationManager = autoclass('android.app.NotificationManager')
Drawable = autoclass("{}.R$drawable".format(activity.getPackageName()))
PendingIntent = autoclass('android.app.PendingIntent')
Intent = autoclass('android.content.Intent')
Toast = autoclass('android.widget.Toast')
BitmapFactory = autoclass('android.graphics.BitmapFactory')

class AndroidNotification(Notification):
    '''
    Implementation of Android notification API.

    .. versionadded:: 1.0.0
    '''

    def __init__(self):
        self._ns = None
        self._channel_id = None

    def _get_notification_service(self):
        if not self._ns:
            self._ns = cast(NotificationManager, activity.getSystemService(
                Context.NOTIFICATION_SERVICE
            ))
        return self._ns

    def _build_notification_channel(self, name, _id):
        '''
        Create a NotificationChannel using channel id of the application
        package name (com.xyz, org.xyz, ...) and channel name same as the
        provided notification title if the API is high enough, otherwise
        do nothing.

        .. versionadded:: 1.4.0
        '''

        if SDK_INT < 26:
            return

        channel = autoclass('android.app.NotificationChannel')
        self._channel_id = activity.getPackageName()
        self._channel_id = self._channel_id + str(_id)

        app_channel = channel(
            self._channel_id, name, NotificationManager.IMPORTANCE_DEFAULT
        )
        self._get_notification_service().createNotificationChannel(
            app_channel
        )
        return app_channel

    @run_on_ui_thread
    def _toast(self, message):
        '''
        Display a popup-like small notification at the bottom of the screen.

        .. versionadded:: 1.4.0
        '''
        Toast.makeText(
            activity,
            cast('java.lang.CharSequence', AndroidString(message)),
            Toast.LENGTH_LONG
        ).show()

    @staticmethod
    def _set_icons(notification, icon=None):
        '''
        Set the small application icon displayed at the top panel together with
        WiFi, battery percentage and time and the big optional icon (preferably
        PNG format with transparent parts) displayed directly in the
        notification body.

        .. versionadded:: 1.4.0
        '''

        app_icon = Drawable.your_icon_name_without_extensionname
        notification.setSmallIcon(app_icon)

        bitmap_icon = app_icon

        if icon is not None:
            bitmap_icon = BitmapFactory.decodeFile(icon)
            notification.setLargeIcon(bitmap_icon)
        elif icon == '':
            # we don't want the big icon set,
            # only the small one in the top panel
            pass
        else:
            bitmap_icon = BitmapFactory.decodeResource(
                python_act.getResources(), app_icon
            )
            notification.setLargeIcon(bitmap_icon)

    def _build_notification(self, title, custom_id):
        '''
        .. versionadded:: 1.4.0
        '''
        if SDK_INT < 26:
            noti = NotificationBuilder(activity)
        else:
            self._channel = self._build_notification_channel(title, custom_id)
            noti = NotificationBuilder(activity, self._channel_id)
        return noti

    @staticmethod
    def _set_open_behavior(notification):

        #Service = autoclass('org.kivy.your_app_name.YourServicenName').mService
        #Service.stopForeground(True)
        '''
        Open the source application when user opens the notification.

        .. versionadded:: 1.4.0
        '''

        # create Intent that navigates back to the application
        app_context = activity.getApplication().getApplicationContext()
        notification_intent = Intent(app_context, python_act)

        # set flags to run our application Activity
        notification_intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
        notification_intent.setAction(Intent.ACTION_MAIN)
        notification_intent.addCategory(Intent.CATEGORY_LAUNCHER)

        # get our application Activity
        pending_intent = PendingIntent.getActivity(
            app_context, 0, notification_intent, 0)

        notification.setContentIntent(pending_intent)
        notification.setAutoCancel(True)

    def _open_notification(self, notification, _id):
        if SDK_INT >= 16:
            notification = notification.build()
        else:
            notification = notification.getNotification()

        self._get_notification_service().notify(_id, notification)

    def _notify(self, **kwargs):
        noti = None
        chan = kwargs.get('chan')
        message = kwargs.get('message').encode('utf-8')
        ticker = kwargs.get('ticker').encode('utf-8')
        title = AndroidString(
            kwargs.get('title', '').encode('utf-8')
        )
        icon = kwargs.get('app_icon')

        # decide whether toast only or proper notification
        if kwargs.get('toast'):
            self._toast(message)
            return
        else:
            noti = self._build_notification(title, chan)

        # set basic properties for notification
        noti.setContentTitle(title)
        noti.setContentText(AndroidString(message))
        noti.setTicker(AndroidString(ticker))

        # set additional flags for notification
        self._set_icons(noti, icon=icon)
        self._set_open_behavior(noti)

        # launch
        self._open_notification(noti, chan)

def instance():
    '''
    Instance for facade proxy.
    '''
    return AndroidNotification()

In your builder.specs enable this line: android.add_resources = data/legal_icons:drawable

you need also to make a dir called data/legal_icons in your project directory and place there your png icon for notifications. It will be then exported during deployment.

THATS IT! Don't forget to remove .pyc files of notification.py in all /platform/android and facades directories and run buildozer debug for the new pyc files to compile again.

It's a pretty hard work around but this is the only way I managed to get notifications to work on many android APIs. Also the kivy version I use is 2.1.0 and plyer 2.1.0 too . I wasn't able to get it run on the never kivy and plyer versions, but you can try yourself maybe it will work for you.

I don't remember exact details what I changed here but this is a working fix which I'm using until now. I'm still running old requirements in buildozer.specs for my app to be as much bug free as possible and not to introduce new problems on every new release of packages:

requirements = python3,kivy==2.1.0,jnius,https://github.com/kivy/python-for-android/archive/refs/tags/v2022.12.20.zip,requests,idna,chardet,android,cryptography,png,pypng,https://github.com/kivy/plyer/archive/refs/tags/2.1.0.zip,kvdroid


The main Service notification needs to be disabled manually by the user to hide it from the screen. There is no other way around I guess.

And here is my service.py if anyone is interested:

from time import sleep
from jnius import autoclass
import socket
import ssl
import configparser
from plyer import notification

# Restarts the service as soon as the script ends.
PythonService = autoclass('org.kivy.android.PythonService')
PythonService.mService.setAutoRestartService(False)
#PythonService.mService.stopForeground(True)
# PythonService has ID 1 - p4a1

# Storing temporary ids of messages in array. They are indexed as templates in a dictionary
msg_storage = []

msg_template = {2: 'Approaching Neburion. Get ready!',
                3: 'You got a gift!',
                4: 'Solar barrier is weakening!',
                5: 'You got a new message',
                6: 'All upgrades done!',
                7: "Your item has been sold in the shop",
                8: 'Merchant appeared in south of Yodanga',
                9: 'You will reach Anabris in 1 hour'
                }

config = configparser.ConfigParser()
# Reads user id form secret for later communication
open('../settings.ini', 'r')
config.read('../settings.ini')
content = config.get('Startup', 'Secret')

if content == 'None':
    user_id = ''
    notify_user = False
else:
    user_id = content[44:]
    notify_user = True

def server_connected():
    global user_id, msg_storage
    try:
        _HOST = socket.gethostbyname('example.com')
    except socket.error:
        return False
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    context = ssl.SSLContext()
    try:
        usr = context.wrap_socket(sock, server_hostname=_HOST)
    except socket.error:
        return False
    try:
        usr.settimeout(5)
        usr.connect((_HOST, 5005))
        usr.settimeout(None)
        usr.send('x'.encode())
        latest_version = usr.recv().decode()
        # add this feature later
            # login_status = '...new version is available to download!'
        usr.send(str('notify' + user_id).encode())
        msg_storage = [int(m) for m in (usr.recv(256).decode()).split()]
        return True
    except socket.error:
        return False

msg_counter = [0,0,0,0,0,0,0,0,0,0]
# Continuously performs connection to the server and obtains IDs of notifications to display for a user
while True:
    # Adjust sleep depending on duration of your service script
    # default should be 300 seconds
    sleep(60)
    if notify_user:
        if server_connected():
            for msg_id in msg_storage:
                if msg_id > 1:
                    msg_counter[msg_id] =  msg_counter[msg_id] + 1
                    notification.notify(chan=msg_id, title=f'channel {msg_id}', message=f'{msg_counter[msg_id]}x {msg_template[msg_id]}')
            msg_storage.clear()

Hope this helps someone!

Screenshot_20230809-172811185~2