Source code for enamlnative.android.android_notification

"""
Copyright (c) 2017-2018, CodeLV.

Distributed under the terms of the MIT License.

The full license is in the file LICENSE, distributed with this software.

Created on Apr 4, 2018


"""
from typing import ClassVar, Optional
from atom.api import Bool, Instance, List, Typed
from enaml.application import deferred_call
from enamlnative.widgets.notification import ProxyNotification
from .android_content import (
    BroadcastReceiver,
    Context,
    Intent,
    PendingIntent,
    SystemService,
)
from .android_image_view import Bitmap, Glide, RequestBuilder, RequestManager
from .android_toolkit_object import AndroidToolkitObject
from .android_utils import Uri
from .app import AndroidApplication
from .bridge import JavaBridgeObject, JavaMethod, JavaStaticMethod

package = "androidx.core.app"


class Notification(JavaBridgeObject):
    """A wrapper for an Android's notification apis"""

    __nativeclass__ = f"{package}.NotificationCompat"

    class Builder(JavaBridgeObject):
        """Builds a notification.

        Notes
        ------
        The constructor automatically sets the when field to
        System.currentTimeMillis() and the audio stream to the STREAM_DEFAULT.

        """

        __nativeclass__ = f"{package}.NotificationCompat$Builder"
        __signature__ = [Context, str]
        addAction = JavaMethod(f"{package}.NotificationCompat$Action")
        addAction_ = JavaMethod("android.R", "java.lang.CharSequence", PendingIntent)
        addInvisibleAction = JavaMethod(f"{package}.NotificationCompat$Action")
        addInvisibleAction_ = JavaMethod(
            "android.R", "java.lang.CharSequence", PendingIntent
        )
        addPerson = JavaMethod(str)
        build = JavaMethod(returns="android.app.Notification")

        setAutoCancel = JavaMethod(bool)
        setBadgeIconType = JavaMethod("android.R")
        setCategory = JavaMethod(str)
        setChannelId = JavaMethod(str)
        setColor = JavaMethod("android.graphics.Color")
        setColorized = JavaMethod(bool)
        setContent = JavaMethod("android.widget.RemoteViews")
        setContentInfo = JavaMethod("java.lang.CharSequence")
        setContentIntent = JavaMethod(PendingIntent)
        setContentText = JavaMethod("java.lang.CharSequence")
        setContentTitle = JavaMethod("java.lang.CharSequence")
        setCustomBigContentView = JavaMethod("android.widget.RemoteViews")
        setCustomContentView = JavaMethod("android.widget.RemoteViews")
        setCustomHeadsUpContentView = JavaMethod("android.widget.RemoteViews")
        setDefaults = JavaMethod(int)
        setDeleteIntent = JavaMethod(PendingIntent)
        setExtras = JavaMethod("android.os.Bundle")
        setFullScreenIntent = JavaMethod(PendingIntent, bool)

        setGroup = JavaMethod(str)
        setGroupAlertBehavior = JavaMethod(int)
        setGroupSummary = JavaMethod(bool)
        setLargeIcon = JavaMethod(Bitmap)
        setLights = JavaMethod("android.graphics.Color", int, int)
        setLocalOnly = JavaMethod(bool)
        setNumber = JavaMethod(int)
        setOngoing = JavaMethod(bool)
        setOnlyAlertOnce = JavaMethod(bool)
        setPriority = JavaMethod(int)
        setProgress = JavaMethod(int, int, bool)
        setPublicVersion = JavaMethod("android.app.Notification")
        setRemoteInputHistory = JavaMethod("Landroid.app.Notification;[")
        setShortcutId = JavaMethod(str)
        setShowWhen = JavaMethod(bool)
        setSmallIcon = JavaMethod("android.R")
        setSmallIcon_ = JavaMethod("android.R", int)
        setSortKey = JavaMethod(str)
        setSound = JavaMethod(Uri)
        setSound_ = JavaMethod(Uri, int)
        setStyle = JavaMethod(f"{package}.NotificationCompat$Style")
        setSubText = JavaMethod("java.lang.CharSequence")
        setTicker = JavaMethod("java.lang.CharSequence", "android.widget.RemoteViews")
        setTicker_ = JavaMethod("java.lang.CharSequence")
        setTimeoutAfter = JavaMethod("long")
        setUsesChronometer = JavaMethod(bool)
        setVibrate = JavaMethod("J[")
        setVisibility = JavaMethod(int)
        setWhen = JavaMethod("long")

        #: Glide Manager for loading bitmaps
        manager = Instance(RequestManager)

        #: BroadcastReceiver for handling actions
        _receivers = List(BroadcastReceiver)

        def _default_manager(self):
            app = AndroidApplication.instance()
            return RequestManager(__id__=Glide.with__(app))

        def load_bitmap(self, src: str) -> Bitmap:
            mgr = self.manager
            assert mgr is not None
            r = RequestBuilder(__id__=mgr.load(src)).asBitmap()
            return Bitmap(__id__=r.__id__)

        def update(
            self,
            title: str = "",
            text: str = "",
            info: str = "",
            sub_text: str = "",
            ticker: str = "",
            importance: int = 3,  # IMPORTANCE_DEFAULT
            group: str = "",
            group_summary: Optional[bool] = None,
            group_alert_behavior: Optional[int] = None,
            small_icon="@mipmap/ic_launcher",
            large_icon="",
            number=None,
            ongoing=None,
            local_only=None,
            style=None,
            color=None,
            colorized=None,
            sound=None,
            light_color="",
            light_on=500,
            light_off=500,
            show_when=None,
            show_stopwatch=False,
            show_progress=False,
            progress_max=100,
            progress_current=0,
            progress_indeterminate=False,
            auto_cancel=True,
            timeout_after=0,
            sort_key="",
            vibration_pattern=[],
            shortcut_id="",
            category="",
            badge_icon_type=None,
            only_alert_once=None,
            notification_options=None,
            actions=None,
        ):
            """Creates and shows a notification. On android 8+ the channel
            must be created.

            Parameters
            ----------
            badge_icon_type: Int
                Sets which icon to display as a badge for this notification.
            category: String
                Set the notification category.
            channel_id: String
                The id of the channel this notification is displayed on
            color: String
                A color string ex #F00 for red
            colorized: Bool
                Set whether this notification should be colorized.
            group: String
                Set this notification to be part of a group of notifications
                sharing the same key.
            group_summary: Bool
                Set this notification to be the group summary for a group of
                notifications.
            group_alert_behavior: Int
                Sets the group alert behavior for this notification.
            title: String
                Set the title (first row) of the notification, in a standard
                notification.
            text: String
                Set the text (second row) of the notification, in a standard
                notification.
            info: String
                Set the large text at the right-hand side of the notification.
            sub_text: String
                Set the third line of text in the platform notification
                template.
            ticker: String
                Sets the "ticker" text which is sent to accessibility services.
            importance: Int
                The importance or priority of the notification
            number: Int
                Set the large number at the right-hand side of the
                notification.
            ongoing: Bool
                Set whether this is an ongoing notification.
            local_only: Bool
                Set whether or not this notification is only relevant to the
                current device.
            style: String
                A style resource string
            small_icon: String
                A resource identifier @drawable/my_drawable
            large_icon: String
                A resource that glide can load (file:// or http://) url
            show_when: Bool
                Control whether the timestamp set with setWhen is shown in the
                content view.
            show_stopwatch: Bool
                Show the when field as a stopwatch.
            show_progress: Bool
                Show a progressbar
            progress_current: Int
                Current progress
            progress_max: Int
                Max progress
            progress_indeterminate: Bool
                Progress should be indeterminate
            light_color: String
                Set the argb value that you would like the LED on the device to
                blink
            light_on: Int
                Set the on duration of the LED
            light_off: Int
                Set the off duration of the LED
            auto_cancel: Bool
                Close automatically when tapped
            only_alert_once: Bool
                Set this flag if you would only like the sound, vibrate and
                ticker to be played if the notification is not already showing
            timeout_after: Long
                Specifies the time at which this notification should be
                canceled, if it is not already canceled.
            sort_key: String
                Set a sort key that orders this notification among other
                notifications from the same package.
            shortcut_id: String
                If this notification is duplicative of a Launcher shortcut,
                sets the id of the shortcut, in case the Launcher wants to hide
                the shortcut.
            sound: String
               Set the Uri of the sound to play.
            vibration_pattern: List of Long
                Set the vibration pattern to use.
            notification_options: Int
                Set the default notification options that will be used.
            actions: List
                A list of actions to handle

            Returns
            --------
            result: Future
                A future that resolves with the builder of the notification
                that was created.

            References
            ----------
            - https://developer.android.com/guide/topics/ui/notifiers/notifications.html
            - https://developer.android.com/training/notify-user/build-notification.html

            """
            if title:
                self.setContentTitle(title)
            if importance:
                self.setPriority(importance)
            if small_icon:
                self.setSmallIcon(small_icon)
            if text:
                self.setContentText(text)
            if sub_text:
                self.setSubText(sub_text)
            if ticker:
                self.setTicker_(ticker)
            if info:
                self.setContentInfo(info)
            if style:
                self.setStyle(style)
            if color:
                self.setColor(color)
            if colorized is not None:
                self.setColorized(bool(colorized))
            if number is not None:
                self.setNumber(int(number))
            if ongoing is not None:
                self.setOngoing(bool(ongoing))
            if only_alert_once is not None:
                self.setOnlyAlertOnce(bool(only_alert_once))
            if local_only is not None:
                self.setLocalOnly(bool(local_only))
            if auto_cancel:
                self.setAutoCancel(bool(auto_cancel))
            if show_when is not None:  # When is shown by default
                self.setShowWhen(bool(show_when))
            if show_stopwatch:
                self.setUsesChronometer(bool(show_stopwatch))
            if show_progress:
                self.setProgress(
                    int(progress_max),
                    int(progress_current),
                    bool(progress_indeterminate),
                )
            if timeout_after:
                self.setTimeoutAfter(int(timeout_after))
            if category:
                self.setCategory(category)
            if group:
                self.setGroup(group)
            if group_summary is not None:
                self.setGroupSummary(bool(group_summary))
            if group_alert_behavior is not None:
                self.setGroupAlertBehavior(int(group_alert_behavior))
            if shortcut_id:
                self.setShortcutId(shortcut_id)
            if large_icon:
                self.setLargeIcon(self.load_bitmap(large_icon))
            if light_color:
                self.setLights(light_color, light_on, light_off)
            if sort_key:
                self.setSortKey(sort_key)
            if sound:
                self.setSound(Uri.parse(sound))
            if vibration_pattern:
                self.setVibrate([int(i) for i in vibration_pattern])
            if badge_icon_type is not None:
                self.setBadgeIconType(int(badge_icon_type))
            if notification_options is not None:
                self.setDefaults(int(notification_options))
            if actions is not None:
                app = AndroidApplication.instance()
                for (action_icon, action_text, action_callback) in actions:
                    action_key = f"com.codelv.enamlnative.Notify{self.__id__}"
                    intent = Intent()
                    intent.setAction(action_key)
                    receiver = BroadcastReceiver.for_action(
                        action_key, action_callback, single_shot=False
                    )
                    self._receivers.append(receiver)
                    self.addAction_(
                        "@mipmap/ic_launcher",
                        action_text,
                        PendingIntent.getBroadcast(app, 0, intent, 0),
                    )

        async def show(self):
            """Build and show this notification"""
            mgr = await NotificationManager.get()
            notificaion_id = await self.build()
            notification = Notification(__id__=notificaion_id)
            mgr.notify(self.__id__, notification)


# Android really messed this one up
[docs]class NotificationManager(JavaBridgeObject): """Android NotificationManager. Use the `show_notification` and `create_channel` class methods. """ IMPORTANCE_NONE = -1000 IMPORTANCE_MIN = 1 IMPORTANCE_LOW = 2 IMPORTANCE_DEFAULT = 3 IMPORTANCE_HIGH = 4 IMPORTANCE_MAX = 5 PRIORITIES = { "low": IMPORTANCE_LOW, "normal": IMPORTANCE_DEFAULT, "high": IMPORTANCE_HIGH, } #: Holds the singleton instance _instance: ClassVar[Optional["NotificationManager"]] = None __nativeclass__ = f"{package}.NotificationManagerCompat" ACTION_BIND_SIDE_CHANNEL = "android.support.BIND_NOTIFICATION_SIDE_CHANNEL" EXTRA_USE_SIDE_CHANNEL = "android.support.useSideChannel" cancel = JavaMethod(str, int) cancel_ = JavaMethod(int) cancelAll = JavaMethod() notify = JavaMethod(int, "android.app.Notification") # type: ignore notify_ = JavaMethod(str, int, "android.app.Notification") from_ = JavaStaticMethod(Context, returns=f"{package}.NotificationManagerCompat") _receivers = List(BroadcastReceiver)
[docs] @classmethod def instance(cls) -> Optional["NotificationManager"]: """Get an instance of this service if it was already requested. You should request it first using `NotificationManager.get()` __Example__ :::python await NotificationManager.get() """ return cls._instance
[docs] @classmethod async def get(cls) -> "NotificationManager": """Acquires the NotificationManager service async.""" app = AndroidApplication.instance() if cls._instance: return cls._instance obj_id = await cls.from_(app) return cls(__id__=obj_id)
[docs] def __init__(self, *args, **kwargs): """Force only one instance to exist""" cls = self.__class__ if cls._instance is not None: name = cls.__name__ raise RuntimeError( f"Only one instance of {name} can exist! " f"Use {name}.instance() instead!" ) super().__init__(*args, **kwargs) cls._instance = self
[docs] @classmethod async def create_channel( cls, channel_id: str, name: str, importance: int = IMPORTANCE_DEFAULT, description: str = "", ) -> Optional["NotificationChannel"]: """Before you can deliver the notification on Android 8.0 and higher, you must register your app's notification channel with the system by passing an instance of NotificationChannel to createNotificationChannel(). Parameters ---------- channel_id: String- Channel ID name: String Channel name importance: Int One of the IMPORTANCE levels description: String Channel description Returns ------- channel: NotificationChannel or None The channel that was created. """ app = AndroidApplication.instance() assert app is not None activity = app.activity assert activity is not None if activity.api_level < 26: return None channel = NotificationChannel(channel_id, name, importance) channel.setDescription(description) mgr = await NotificationChannelManager.get() mgr.createNotificationChannel(channel) return channel
[docs] @classmethod def show_notification(cls, channel_id: int, *args, **kwargs): """Create and show a Notification. See `Notification.Builder.update` for a list of accepted parameters. """ app = AndroidApplication.instance() builder = Notification.Builder(app, channel_id) builder.update(*args, **kwargs) return builder.show()
[docs] @classmethod async def cancel_notification(cls, notification_or_id, tag=None): """Cancel the notification. Parameters ---------- notification_or_id: Notification.Builder or int The notification or id of a notification to clear tag: String The tag of the notification to clear """ mgr = await cls.get() if isinstance(notification_or_id, JavaBridgeObject): nid = notification_or_id.__id__ else: nid = notification_or_id if tag is None: mgr.cancel_(nid) else: mgr.cancel(tag, nid)
class NotificationChannel(JavaBridgeObject): """Required for android 26 and up.""" __nativeclass__ = "android.app.NotificationChannel" __signature__ = [str, "java.lang.CharSequence", int] setGroup = JavaMethod(str) setLightColor = JavaMethod("android.graphics.Color") setBypassDnd = JavaMethod(bool) setDescription = JavaMethod(str) setImportance = JavaMethod(int) setName = JavaMethod("java.lang.CharSequence") showBadge = JavaMethod(bool) setVibrationPattern = JavaMethod("J[") class NotificationChannelManager(SystemService): SERVICE_TYPE = Context.NOTIFICATION_SERVICE __nativeclass__ = "android.app.NotificationChannelManager" # This is not in the docs createNotificationChannel = JavaMethod(NotificationChannel)
[docs]class AndroidNotification(AndroidToolkitObject, ProxyNotification): """An Android implementation of an Enaml ProxyNotification.""" #: A reference to the widget created by the proxy. builder = Typed(Notification.Builder) #: Keep track of shown = Bool() # ------------------------------------------------------------------------- # Initialization API # -------------------------------------------------------------------------
[docs] def create_widget(self): """The notification is created in init_layout""" pass
[docs] def init_layout(self): """Create the notification in the top down pass if show = True""" d = self.declaration self.create_notification() if d.show: self.set_show(d.show)
[docs] def destroy(self): """A reimplemented destructor that cancels the notification before destroying. """ builder = self.builder async def cancel_notification(): await NotificationManager.cancel_notification(builder) del self.builder deferred_call(cancel_notification) super().destroy()
# ------------------------------------------------------------------------- # Notification API # -------------------------------------------------------------------------
[docs] def create_notification(self): """Instead of the typical create_widget we use `create_notification` because after it's closed it needs created again. """ d = self.declaration builder = self.builder = Notification.Builder(self.get_context(), d.channel_id) d = self.declaration # Apply any custom settings if d.settings: builder.update(**d.settings) for k, v in self.get_declared_items(): handler = getattr(self, f"set_{k}") handler(v) builder.setSmallIcon(d.icon or "@mipmap/ic_launcher") # app = self.get_context() # intent = Intent() # intent.setClass(app, ) # builder.setContentIntent(PendingIntent.getActivity(app, 0, intent, 0)) #: Set custom content if present for view in self.child_widgets(): builder.setCustomContentView(view) break
[docs] def get_declared_items(self): """Get the members that were set in the enamldef block for this Declaration. Returns ------- result: List of (k,v) pairs that were defined for this widget in enaml List of keys and values """ d = self.declaration engine = d._d_engine if engine: for k, h in engine._handlers.items(): # Handlers with read operations if not h.read_pair: continue v = getattr(d, k) if k in ("show", "icon", "settings"): continue # We set these explicitly yield (k, v)
[docs] def refresh(self): """If the""" if self.shown: self.builder.show()
# ------------------------------------------------------------------------- # ProxyNotification API # ------------------------------------------------------------------------- def set_channel_id(self, channel_id): self.builder.setChannelId(channel_id) self.refresh() def set_color(self, color): self.builder.setColor(color) self.refresh() def set_title(self, title): self.builder.setContentTitle(title) self.refresh() def set_info(self, info): self.builder.setContentInfo(info) self.refresh() def set_text(self, text): self.builder.setContentText(text) self.refresh() def set_sub_text(self, sub_text): self.builder.setSubText(sub_text) self.refresh() def set_priority(self, priority): self.builder.setPriority(Notification.PRIORITIES[priority]) self.refresh() def set_show_progress(self, show): self.set_progress(self.declaration.progress) def set_show_when(self, show): self.builder.setShowWhen(show) def set_progress(self, progress): d = self.declaration if d.show_progress: self.builder.setProgress(100, progress, d.progress_indeterminate) else: self.builder.setProgress(0, 0, False) self.refresh() def set_progress_indeterminate(self, indeterminate): self.set_progress(self.declaration.progress) def set_style(self, style): self.builder.setStyle(style) self.refresh() def set_settings(self, settings): self.builder.update(**settings) self.refresh() def set_show(self, show): if show: self.shown = True self.builder.show() else: self.shown = False NotificationManager.cancel_notification(self.builder) # Androids destroys it so create a new one self.create_notification()