diff --git a/android_notify/an_types.py b/android_notify/an_types.py index d24744d..96676bb 100644 --- a/android_notify/an_types.py +++ b/android_notify/an_types.py @@ -1,6 +1,7 @@ """For autocomplete Storing Reference to Available Methods""" from typing import Literal -Importance = Literal['urgent','high','medium','low','none'] + +Importance = Literal['urgent', 'high', 'medium', 'low', 'none'] """ :argument urgent - Makes a sound and appears as a heads-up notification. @@ -13,220 +14,301 @@ :argument urgent - Makes no sound and doesn't in the status bar or shade. """ + # For Dev # Idea for typing autocompletion and reference -class Bundle: - def putString(self,key,value): - print(f"[MOCK] Bundle.putString called with key={key}, value={value}") - - def putInt(self,key,value): - print(f"[MOCK] Bundle.putInt called with key={key}, value={value}") - -class String(str): +class Bundle: + def putString(self, key, value): + print(f"[MOCK] Bundle.putString called with key={key}, value={value}") + + def putInt(self, key, value): + print(f"[MOCK] Bundle.putInt called with key={key}, value={value}") + + +class String(str): def __new__(cls, value): print(f"[MOCK] String created with value={value}") return str.__new__(cls, value) - -class Intent: - def __init__(self,context,activity): - self.obj={} - print(f"[MOCK] Intent initialized with context={context}, activity={activity}") - - def setAction(self,action): - print(f"[MOCK] Intent.setAction called with: {action}") - return self - - def setFlags(self,intent_flag): - print(f"[MOCK] Intent.setFlags called with: {intent_flag}") - return self - - def getAction(self): + + +class Intent: + FLAG_ACTIVITY_NEW_TASK = 'FACADE_FLAG_ACTIVITY_NEW_TASK' + CATEGORY_DEFAULT = 'FACADE_FLAG_CATEGORY_DEFAULT' + + def __init__(self, context='', activity=''): + self.obj = {} + print(f"[MOCK] Intent initialized with context={context}, activity={activity}") + + def setAction(self, action): + print(f"[MOCK] Intent.setAction called with: {action}") + return self + + def addFlags(self, *flags): + print(f"[MOCK] Intent.addFlags called with: {flags}") + print(flags) + return self + + def setData(self, uri): + print(f"[MOCK] Intent.setData called with: {uri}") + return self + + def setFlags(self, intent_flag): + print(f"[MOCK] Intent.setFlags called with: {intent_flag}") + return self + + def addCategory(self, intent_category): + print(f"[MOCK] Intent.addCategory called with: {intent_category}") + print(intent_category) + return self + + def getAction(self): print("[MOCK] Intent.getAction called") - return self - - def getStringExtra(self,key): + return self + + def getStringExtra(self, key): print(f"[MOCK] Intent.getStringExtra called with key={key}") - return self - - def putExtra(self,key,value): + return self + + def putExtra(self, key, value): self.obj[key] = value - print(f"[MOCK] Intent.putExtra called with key={key}, value={value}") - - def putExtras(self,bundle:Bundle): + print(f"[MOCK] Intent.putExtra called with key={key}, value={value}") + + def putExtras(self, bundle: Bundle): self.obj['bundle'] = bundle - print(f"[MOCK] Intent.putExtras called with bundle={bundle}") - -class PendingIntent: - FLAG_IMMUTABLE='' - FLAG_UPDATE_CURRENT='' - - def getActivity(self,context,value,action_intent,pending_intent_type): - print(f"[MOCK] PendingIntent.getActivity called with context={context}, value={value}, action_intent={action_intent}, type={pending_intent_type}") - -class BitmapFactory: - def decodeStream(self,stream): - print(f"[MOCK] BitmapFactory.decodeStream called with stream={stream}") - -class BuildVersion: - SDK_INT=0 - -class NotificationManager: - pass - -class NotificationChannel: - def __init__(self,channel_id,channel_name,importance): - self.description = None + print(f"[MOCK] Intent.putExtras called with bundle={bundle}") + + +class PendingIntent: + FLAG_IMMUTABLE = '' + FLAG_UPDATE_CURRENT = '' + + def getActivity(self, context, value, action_intent, pending_intent_type): + print( + f"[MOCK] PendingIntent.getActivity called with context={context}, value={value}, action_intent={action_intent}, type={pending_intent_type}") + + +class BitmapFactory: + def decodeStream(self, stream): + print(f"[MOCK] BitmapFactory.decodeStream called with stream={stream}") + + +class BuildVersion: + SDK_INT = 0 + +class Manifest: + POST_NOTIFICATIONS = 'FACADE_IMPORT' + +class Settings: + ACTION_APP_NOTIFICATION_SETTINGS = 'FACADE_IMPORT_ACTION_APP_NOTIFICATION_SETTINGS' + EXTRA_APP_PACKAGE = 'FACADE_IMPORT_EXTRA_APP_PACKAGE' + ACTION_APPLICATION_DETAILS_SETTINGS = 'FACADE_IMPORT_ACTION_APPLICATION_DETAILS_SETTINGS' + +class Uri: + def __init__(self,package_name): + print("FACADE_URI") + +class NotificationManager: + pass + + +class NotificationChannel: + def __init__(self, channel_id, channel_name, importance): + self.description = None self.channel_id = channel_id self.channel = None - print(f"[MOCK] NotificationChannel initialized with id={channel_id}, name={channel_name}, importance={importance}") - - def createNotificationChannel(self, channel): - self.channel=channel - print(f"[MOCK] NotificationChannel.createNotificationChannel called with channel={channel}") - - def getNotificationChannel(self, channel_id): - self.channel_id=channel_id - print(f"[MOCK] NotificationChannel.getNotificationChannel called with id={channel_id}") - - def setDescription(self, description): - self.description=description - print(f"[MOCK] NotificationChannel.setDescription called with description={description}") - - def getId(self): + print( + f"[MOCK] NotificationChannel initialized with id={channel_id}, name={channel_name}, importance={importance}") + + def createNotificationChannel(self, channel): + self.channel = channel + print(f"[MOCK] NotificationChannel.createNotificationChannel called with channel={channel}") + + def getNotificationChannel(self, channel_id): + self.channel_id = channel_id + print(f"[MOCK] NotificationChannel.getNotificationChannel called with id={channel_id}") + + def setDescription(self, description): + self.description = description + print(f"[MOCK] NotificationChannel.setDescription called with description={description}") + + def getId(self): print(f"[MOCK] NotificationChannel.getId called, returning {self.channel_id}") - return self.channel_id - -class IconCompat: - def createWithBitmap(self,bitmap): - print(f"[MOCK] IconCompat.createWithBitmap called with bitmap={bitmap}") - -class Color: - def __init__(self): - print("[MOCK] Color initialized") - def parseColor(self,color:str): + return self.channel_id + + +class IconCompat: + def createWithBitmap(self, bitmap): + print(f"[MOCK] IconCompat.createWithBitmap called with bitmap={bitmap}") + + +class Color: + def __init__(self): + print("[MOCK] Color initialized") + + def parseColor(self, color: str): print(f"[MOCK] Color.parseColor called with color={color}") - return self - -class RemoteViews: - def __init__(self, package_name, small_layout_id): - print(f"[MOCK] RemoteViews initialized with package_name={package_name}, layout_id={small_layout_id}") - def createWithBitmap(self,bitmap): - print(f"[MOCK] RemoteViews.createWithBitmap called with bitmap={bitmap}") - def setTextViewText(self,id, text): - print(f"[MOCK] RemoteViews.setTextViewText called with id={id}, text={text}") - def setTextColor(self,id, color:Color): - print(f"[MOCK] RemoteViews.setTextColor called with id={id}, color={color}") - -class NotificationManagerCompat: - IMPORTANCE_HIGH=4 - IMPORTANCE_DEFAULT=3 - IMPORTANCE_LOW='' - IMPORTANCE_MIN='' - IMPORTANCE_NONE='' - -class NotificationCompat: - DEFAULT_ALL=3 - PRIORITY_HIGH=4 - PRIORITY_DEFAULT = '' - PRIORITY_LOW='' - PRIORITY_MIN='' - -class MActions: - def clear(self): - """This Removes all buttons""" - print('[MOCK] MActions.clear called') - -class NotificationCompatBuilder: - def __init__(self,context,channel_id): - self.mActions = MActions() - print(f"[MOCK] NotificationCompatBuilder initialized with context={context}, channel_id={channel_id}") - def setProgress(self,max_value,current_value,endless): - print(f"[MOCK] setProgress called with max={max_value}, current={current_value}, endless={endless}") - def setStyle(self,style): - print(f"[MOCK] setStyle called with style={style}") - def setContentTitle(self,title): - print(f"[MOCK] setContentTitle called with title={title}") - def setContentText(self,text): - print(f"[MOCK] setContentText called with text={text}") - def setSmallIcon(self,icon): - print(f"[MOCK] setSmallIcon called with icon={icon}") - def setLargeIcon(self,icon): - print(f"[MOCK] setLargeIcon called with icon={icon}") - def setAutoCancel(self,auto_cancel:bool): - print(f"[MOCK] setAutoCancel called with auto_cancel={auto_cancel}") - def setPriority(self,priority): - print(f"[MOCK] setPriority called with priority={priority}") - def setDefaults(self,defaults): - print(f"[MOCK] setDefaults called with defaults={defaults}") - def setOngoing(self,persistent:bool): - print(f"[MOCK] setOngoing called with persistent={persistent}") - def setOnlyAlertOnce(self,state): - print(f"[MOCK] setOnlyAlertOnce called with state={state}") - def build(self): - print("[MOCK] build called") - def setContentIntent(self,pending_action_intent:PendingIntent): - print(f"[MOCK] setContentIntent called with {pending_action_intent}") - def addAction(self,icon_int,action_text,pending_action_intent): - print(f"[MOCK] addAction called with icon={icon_int}, text={action_text}, intent={pending_action_intent}") - def setShowWhen(self,state): - print(f"[MOCK] setShowWhen called with state={state}") - def setWhen(self,time_ms): - print(f"[MOCK] setWhen called with time_ms={time_ms}") - def setCustomContentView(self,layout): - print(f"[MOCK] setCustomContentView called with layout={layout}") - def setCustomBigContentView(self,layout): - print(f"[MOCK] setCustomBigContentView called with layout={layout}") - def setSubText(self,text): - print(f"[MOCK] setSubText called with text={text}") - def setColor(self, color:Color) -> None: - print(f"[MOCK] setColor called with color={color}") + return self + + +class RemoteViews: + def __init__(self, package_name, small_layout_id): + print(f"[MOCK] RemoteViews initialized with package_name={package_name}, layout_id={small_layout_id}") + + def createWithBitmap(self, bitmap): + print(f"[MOCK] RemoteViews.createWithBitmap called with bitmap={bitmap}") + + def setTextViewText(self, id, text): + print(f"[MOCK] RemoteViews.setTextViewText called with id={id}, text={text}") + + def setTextColor(self, id, color: Color): + print(f"[MOCK] RemoteViews.setTextColor called with id={id}, color={color}") + + +class NotificationManagerCompat: + IMPORTANCE_HIGH = 4 + IMPORTANCE_DEFAULT = 3 + IMPORTANCE_LOW = '' + IMPORTANCE_MIN = '' + IMPORTANCE_NONE = '' + + +class NotificationCompat: + DEFAULT_ALL = 3 + PRIORITY_HIGH = 4 + PRIORITY_DEFAULT = '' + PRIORITY_LOW = '' + PRIORITY_MIN = '' + + +class MActions: + def clear(self): + """This Removes all buttons""" + print('[MOCK] MActions.clear called') + + +class NotificationCompatBuilder: + def __init__(self, context, channel_id): + self.mActions = MActions() + print(f"[MOCK] NotificationCompatBuilder initialized with context={context}, channel_id={channel_id}") + + def setProgress(self, max_value, current_value, endless): + print(f"[MOCK] setProgress called with max={max_value}, current={current_value}, endless={endless}") + + def setStyle(self, style): + print(f"[MOCK] setStyle called with style={style}") + + def setContentTitle(self, title): + print(f"[MOCK] setContentTitle called with title={title}") + + def setContentText(self, text): + print(f"[MOCK] setContentText called with text={text}") + + def setSmallIcon(self, icon): + print(f"[MOCK] setSmallIcon called with icon={icon}") + + def setLargeIcon(self, icon): + print(f"[MOCK] setLargeIcon called with icon={icon}") + + def setAutoCancel(self, auto_cancel: bool): + print(f"[MOCK] setAutoCancel called with auto_cancel={auto_cancel}") + + def setPriority(self, priority: Importance): + print(f"[MOCK] setPriority called with priority={priority}") + + def setDefaults(self, defaults): + print(f"[MOCK] setDefaults called with defaults={defaults}") + + def setOngoing(self, persistent: bool): + print(f"[MOCK] setOngoing called with persistent={persistent}") + + def setOnlyAlertOnce(self, state): + print(f"[MOCK] setOnlyAlertOnce called with state={state}") + + def build(self): + print("[MOCK] build called") + + def setContentIntent(self, pending_action_intent: PendingIntent): + print(f"[MOCK] setContentIntent called with {pending_action_intent}") + + def addAction(self, icon_int, action_text, pending_action_intent): + print(f"[MOCK] addAction called with icon={icon_int}, text={action_text}, intent={pending_action_intent}") + + def setShowWhen(self, state): + print(f"[MOCK] setShowWhen called with state={state}") + + def setWhen(self, time_ms): + print(f"[MOCK] setWhen called with time_ms={time_ms}") + + def setCustomContentView(self, layout): + print(f"[MOCK] setCustomContentView called with layout={layout}") + + def setCustomBigContentView(self, layout): + print(f"[MOCK] setCustomBigContentView called with layout={layout}") + + def setSubText(self, text): + print(f"[MOCK] setSubText called with text={text}") + + def setColor(self, color: Color) -> None: + print(f"[MOCK] setColor called with color={color}") + class NotificationCompatBigTextStyle: - def bigText(self,body): + def bigText(self, body): print(f"[MOCK] NotificationCompatBigTextStyle.bigText called with body={body}") return self + class NotificationCompatBigPictureStyle: - def bigPicture(self,bitmap): + def bigPicture(self, bitmap): print(f"[MOCK] NotificationCompatBigPictureStyle.bigPicture called with bitmap={bitmap}") return self + class NotificationCompatInboxStyle: - def addLine(self,line): + def addLine(self, line): print(f"[MOCK] NotificationCompatInboxStyle.addLine called with line={line}") return self + class NotificationCompatDecoratedCustomViewStyle: def __init__(self): print("[MOCK] NotificationCompatDecoratedCustomViewStyle initialized") + class Permission: - POST_NOTIFICATIONS='' + POST_NOTIFICATIONS = '' + -def check_permission(permission:Permission.POST_NOTIFICATIONS): +def check_permission(permission: Permission.POST_NOTIFICATIONS): print(f"[MOCK] check_permission called with {permission}") print(permission) + def request_permissions(_list: [], _callback): print(f"[MOCK] request_permissions called with {_list}") _callback() + class AndroidActivity: - def bind(self,on_new_intent): + def bind(self, on_new_intent): print(f"[MOCK] AndroidActivity.bind called with {on_new_intent}") - def unbind(self,on_new_intent): + + def unbind(self, on_new_intent): print(f"[MOCK] AndroidActivity.unbind called with {on_new_intent}") + class PythonActivity: def __init__(self): - print("[MOCK] PythonActivity initialized") - + print("[MOCK] PythonActivity initialized") + class DummyIcon: icon = 101 + def __init__(self): print("[MOCK] DummyIcon initialized") - + + class Context: def __init__(self): print("[MOCK] Context initialized") @@ -245,7 +327,7 @@ def getResources(): @staticmethod def getPackageName(): print("[MOCK] Context.getPackageName called") - return None # TODO get package name from buildozer.spec file + return None # TODO get package name from buildozer.spec file -#Now writing Knowledge from errors +# Now writing Knowledge from errors # notify.(int, Builder.build()) # must be int diff --git a/android_notify/an_utils.py b/android_notify/an_utils.py index 77aa988..56c448c 100644 --- a/android_notify/an_utils.py +++ b/android_notify/an_utils.py @@ -4,16 +4,18 @@ from .config import autoclass from .an_types import Importance from .config import ( - get_python_activity_context, app_storage_path,ON_ANDROID, - BitmapFactory, BuildVersion, Bundle, - NotificationManagerClass,AndroidNotification,Intent, Settings, Uri, String, Manifest - ) + get_python_activity_context, app_storage_path, ON_ANDROID, + BitmapFactory, BuildVersion, Bundle, + NotificationManagerClass, AndroidNotification, Intent, Settings, Uri, String, Manifest + +) if ON_ANDROID: Color = autoclass('android.graphics.Color') else: from .an_types import Color + def can_accept_arguments(func, *args, **kwargs): try: sig = inspect.signature(func) @@ -22,11 +24,13 @@ def can_accept_arguments(func, *args, **kwargs): except TypeError: return False + if ON_ANDROID: context = get_python_activity_context() else: context = None + def get_android_importance(importance: Importance): """ Returns Android Importance Values @@ -50,6 +54,7 @@ def get_android_importance(importance: Importance): return value # side-note 'medium' = NotificationCompat.PRIORITY_LOW and 'low' = NotificationCompat.PRIORITY_MIN # weird but from docs + def generate_channel_id(channel_name: str) -> str: """ Generate a readable and consistent channel ID from a channel name. @@ -68,8 +73,9 @@ def generate_channel_id(channel_name: str) -> str: channel_id = channel_id.strip('_') return channel_id[:50] + def get_img_from_path(relative_path): - app_folder = os.path.join(app_storage_path(), 'app') + app_folder = os.path.join(app_storage_path(), 'app') img_full_path = os.path.join(app_folder, relative_path) img_name = os.path.basename(img_full_path) if not os.path.exists(img_full_path): @@ -84,6 +90,7 @@ def get_img_from_path(relative_path): return get_bitmap_from_path(img_full_path) # TODO test with a badly written Image and catch error + def setLayoutText(layout, id, text, color): # checked if self.title_color available before entering method if id and text: @@ -91,6 +98,7 @@ def setLayoutText(layout, id, text, color): if color: layout.setTextColor(id, Color.parseColor(color)) + def get_bitmap_from_url(url, callback, logs): """Gets Bitmap from url @@ -119,6 +127,7 @@ def get_bitmap_from_url(url, callback, logs): print('Error Type ', extracting_bitmap_frm_URL_error) print('Failed to get Bitmap from URL ', traceback.format_exc()) + def add_data_to_intent(intent, title): """Persist Some data to notification object for later use""" bundle = Bundle() @@ -127,13 +136,14 @@ def add_data_to_intent(intent, title): bundle.putInt("notify_id", 101) intent.putExtras(bundle) + def get_sound_uri(res_sound_name): - if not res_sound_name: - return None + if not res_sound_name: # Incase it's None + return None + + package_name = context.getPackageName() + return Uri.parse(f"android.resource://{package_name}/raw/{res_sound_name}") - package_name = context.getPackageName() - Uri = autoclass('android.net.Uri') - return Uri.parse(f"android.resource://{package_name}/raw/{res_sound_name}") def get_package_path(): """ @@ -142,9 +152,11 @@ def get_package_path(): """ return os.path.dirname(os.path.abspath(__file__)) + def get_bitmap_from_path(img_full_path): - uri = Uri.parse(f"file://{img_full_path}") - return BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri)) + uri = Uri.parse(f"file://{img_full_path}") + return BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri)) + def icon_finder(icon_name): """Get the full path to an icon file.""" @@ -156,34 +168,43 @@ def icon_finder(icon_name): package_dir = get_package_path() return os.path.join(package_dir, "fallback-icons", icon_name) + def can_show_permission_request_popup(): """ Check if we can show permission request popup for POST_NOTIFICATIONS :return: bool """ - if not ON_ANDROID or BuildVersion.SDK_INT < 33: + if not ON_ANDROID: + return False + + if BuildVersion.SDK_INT < 33: return False return context.shouldShowRequestPermissionRationale(Manifest.POST_NOTIFICATIONS) - + + def open_settings_screen(): + if not context: + print("android_notify - Can't open settings screen, No context [not On Android]") + return None intent = Intent() intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - package_name = String(context.getPackageName()) # String() is very important else fails silently with a toast + package_name = String(context.getPackageName()) # String() is very important else fails silently with a toast # saying "The app wasn't found in the list of installed apps" - Xiaomi or "unable to find application to perform this action" - Samsung and Techno - if BuildVersion.SDK_INT >= 26: # Android 8.0 - android.os.Build.VERSION_CODES.O + if BuildVersion.SDK_INT >= 26: # Android 8.0 - android.os.Build.VERSION_CODES.O intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS) intent.putExtra(Settings.EXTRA_APP_PACKAGE, package_name) - elif BuildVersion.SDK_INT >= 22: # Android 5.0 - Build.VERSION_CODES.LOLLIPOP + elif BuildVersion.SDK_INT >= 22: # Android 5.0 - Build.VERSION_CODES.LOLLIPOP intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS") intent.putExtra("app_package", package_name) intent.putExtra("app_uid", context.getApplicationInfo().uid) - else: # Last Retort is to open App Settings Screen + else: # Last Retort is to open App Settings Screen intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intent.addCategory(Intent.CATEGORY_DEFAULT) intent.setData(Uri.parse("package:" + package_name)) context.startActivity(intent) + return None # https://stackoverflow.com/a/45192258/19961621 diff --git a/android_notify/config.py b/android_notify/config.py index 8428834..2b9444d 100644 --- a/android_notify/config.py +++ b/android_notify/config.py @@ -3,11 +3,13 @@ ON_ANDROID = False __version__ = "1.60.4.dev0" + def on_flet_app(): return os.getenv("MAIN_ACTIVITY_HOST_CLASS_NAME") + def get_activity_class_name(): - ACTIVITY_CLASS_NAME = os.getenv("MAIN_ACTIVITY_HOST_CLASS_NAME") # flet python + ACTIVITY_CLASS_NAME = os.getenv("MAIN_ACTIVITY_HOST_CLASS_NAME") # flet python if not ACTIVITY_CLASS_NAME: try: from android import config @@ -22,7 +24,7 @@ def get_activity_class_name(): else: # print('Not on Flet android env...\n') try: - import kivy #TODO find var for kivy + import kivy # TODO find var for kivy from jnius import cast, autoclass except Exception as e: print('android-notify: No pjnius, not on android') @@ -47,14 +49,14 @@ def get_activity_class_name(): Settings = autoclass("android.provider.Settings") Uri = autoclass("android.net.Uri") Manifest = autoclass('android.Manifest$permission') - + ON_ANDROID = bool(RemoteViews) except Exception as e: from .an_types import * - #if hasattr(e,'name') and e.name != 'android' : - print('Exception: ',e) - print(traceback.format_exc()) + if hasattr(e, 'name') and e.name != 'android': + print('Exception: ', e) + print(traceback.format_exc()) if ON_ANDROID: try: @@ -65,19 +67,20 @@ def get_activity_class_name(): NotificationCompatBigTextStyle = autoclass('android.app.Notification$BigTextStyle') NotificationCompatBigPictureStyle = autoclass('android.app.Notification$BigPictureStyle') NotificationCompatInboxStyle = autoclass('android.app.Notification$InboxStyle') - #NotificationCompatDecoratedCustomViewStyle = autoclass('androidx.core.app.NotificationCompat$DecoratedCustomViewStyle') + # NotificationCompatDecoratedCustomViewStyle = autoclass('androidx.core.app.NotificationCompat$DecoratedCustomViewStyle') except Exception as styles_import_error: - print('styles_import_error: ',styles_import_error) - + print('styles_import_error: ', styles_import_error) from .an_types import * else: from .an_types import * + def from_service_file(): return 'PYTHON_SERVICE_ARGUMENT' in os.environ + run_on_ui_thread = None if on_flet_app() or from_service_file() or not ON_ANDROID: def run_on_ui_thread(func): @@ -88,9 +91,10 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper -else:# TODO find var for kivy +else: # TODO find var for kivy from android.runnable import run_on_ui_thread + def get_python_activity(): if not ON_ANDROID: from .an_types import PythonActivity @@ -101,12 +105,15 @@ def get_python_activity(): else: PythonActivity = autoclass(ACTIVITY_CLASS_NAME + '.PythonActivity') return PythonActivity + + def get_python_service(): if not ON_ANDROID: return None PythonService = autoclass(get_activity_class_name() + '.PythonService') return PythonService.mService + def get_python_activity_context(): if not ON_ANDROID: from .an_types import Context @@ -126,12 +133,14 @@ def get_python_activity_context(): else: context = None + def get_notification_manager(): if not ON_ANDROID: return None notification_service = context.getSystemService(context.NOTIFICATION_SERVICE) return cast(NotificationManagerClass, notification_service) + def app_storage_path(): if on_flet_app(): return os.path.join(context.getFilesDir().getAbsolutePath(), 'flet') diff --git a/android_notify/core.py b/android_notify/core.py index 3300e47..1a8ba34 100644 --- a/android_notify/core.py +++ b/android_notify/core.py @@ -2,17 +2,20 @@ import random import os, traceback from .config import get_python_activity, Manifest + ON_ANDROID = False + def on_flet_app(): return os.getenv("MAIN_ACTIVITY_HOST_CLASS_NAME") try: - from jnius import autoclass # Needs Java to be installed + from jnius import autoclass # Needs Java to be installed + PythonActivity = get_python_activity() - context = PythonActivity.mActivity # Get the app's context + context = PythonActivity.mActivity # Get the app's context NotificationChannel = autoclass('android.app.NotificationChannel') String = autoclass('java.lang.String') Intent = autoclass('android.content.Intent') @@ -20,14 +23,15 @@ def on_flet_app(): BitmapFactory = autoclass('android.graphics.BitmapFactory') BuildVersion = autoclass('android.os.Build$VERSION') Notification = autoclass("android.app.Notification") - ON_ANDROID=True + ON_ANDROID = True except Exception as e: traceback.print_exc() - print(e,'\nThis Package Only Runs on Android !!! ---> Check "https://github.com/Fector101/android_notify/" to see design patterns and more info.\n') + print(e, + '\nThis Package Only Runs on Android !!! ---> Check "https://github.com/Fector101/android_notify/" to see design patterns and more info.\n') if ON_ANDROID: try: - NotificationManagerCompat = autoclass('android.app.NotificationManager') + NotificationManagerCompat = autoclass('android.app.NotificationManager') # Notification Design NotificationCompatBuilder = autoclass('android.app.Notification$Builder') NotificationCompatBigTextStyle = autoclass('android.app.Notification$BigTextStyle') @@ -43,98 +47,109 @@ def on_flet_app(): def get_app_root_path(): path = '' if on_flet_app(): - path= os.path.join(context.getFilesDir().getAbsolutePath(),'flet') + path = os.path.join(context.getFilesDir().getAbsolutePath(), 'flet') else: try: - from android.storage import app_storage_path # type: ignore + from android.storage import app_storage_path # type: ignore path = app_storage_path() except Exception as e: - print('android-notify- Error getting apk main file path: ',e) + print('android-notify- Error getting apk main file path: ', e) return './' - return os.path.join(path,'app') + return os.path.join(path, 'app') -def asks_permission_if_needed(no_androidx=False): + +def asks_permission_if_needed(legacy=False, no_androidx=False): """ Ask for permission to send notifications if needed. + legacy parameter will replace no_androidx parameter in Future Versions """ - if BuildVersion.SDK_INT < 33 or not ON_ANDROID: + if not ON_ANDROID: + print("android_notify- Can't ask permission when not on android") + return None + + if BuildVersion.SDK_INT < 33: + print("android_notify- On android 12 or less don't need permission") return True - + if not can_show_permission_request_popup(): - print("""android_notify- Permission to send notifications has been denied permanently. Please enable it from settings. - This happens when the user denies permission twice from the popup.""") + print("""android_notify- Permission to send notifications has been denied permanently. +This happens when the user denies permission twice from the popup. +Opening notification settings... +""") open_settings_screen() - return - - - if on_flet_app() or no_androidx: + return None + + if on_flet_app() or no_androidx or legacy: Activity = autoclass("android.app.Activity") PackageManager = autoclass("android.content.pm.PackageManager") - + permission = Manifest.POST_NOTIFICATIONS granted = context.checkSelfPermission(permission) - if granted != PackageManager.PERMISSION_GRANTED: context.requestPermissions([permission], 101) - else: # android package is from p4a which is for kivy + else: # android package is from p4a which is for kivy try: - from android.permissions import request_permissions, Permission,check_permission # type: ignore - permissions=[Permission.POST_NOTIFICATIONS] + from android.permissions import request_permissions, Permission, check_permission # type: ignore + permissions = [Permission.POST_NOTIFICATIONS] if not all(check_permission(p) for p in permissions): request_permissions(permissions) except Exception as e: print("android_notify- error trying to request notification access: ", e) + def get_image_uri(relative_path): """ Get the absolute URI for an image in the assets folder. :param relative_path: The relative path to the image (e.g., 'assets/imgs/icon.png'). :return: Absolute URI java Object (e.g., 'file:///path/to/file.png'). """ - app_root_path = get_app_root_path() + app_root_path = get_app_root_path() output_path = os.path.join(app_root_path, relative_path) # print(output_path,'output_path') # /data/user/0/org.laner.lan_ft/files/app/assets/imgs/icon.png - + if not os.path.exists(output_path): raise FileNotFoundError(f"\nImage not found at path: {output_path}\n") - + Uri = autoclass('android.net.Uri') return Uri.parse(f"file://{output_path}") + def get_icon_object(uri): BitmapFactory = autoclass('android.graphics.BitmapFactory') IconCompat = autoclass('androidx.core.graphics.drawable.IconCompat') - bitmap= BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri)) + bitmap = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri)) return IconCompat.createWithBitmap(bitmap) -def insert_app_icon(builder,custom_icon_path): + +def insert_app_icon(builder, custom_icon_path): if custom_icon_path: try: uri = get_image_uri(custom_icon_path) icon = get_icon_object(uri) builder.setSmallIcon(icon) except Exception as e: - print('android_notify- error: ',e) + print('android_notify- error: ', e) builder.setSmallIcon(context.getApplicationInfo().icon) else: # print('Found res icon -->',context.getApplicationInfo().icon,'<--') builder.setSmallIcon(context.getApplicationInfo().icon) + def send_notification( - title:str, - message:str, - style=None, - img_path=None, - channel_name="Default Channel", - channel_id:str="default_channel", - custom_app_icon_path="", - - big_picture_path='', - large_icon_path='', - big_text="", - lines="" - ): + title: str, + message: str, + style=None, + img_path=None, + channel_name="Default Channel", + channel_id: str = "default_channel", + custom_app_icon_path="", + + big_picture_path='', + large_icon_path='', + big_text="", + lines="" +): """ Send a notification on Android. @@ -145,49 +160,51 @@ def send_notification( :param channel_id: Notification channel ID.(Default is lowercase channel name arg in lowercase) """ if not ON_ANDROID: - print('This Package Only Runs on Android !!! ---> Check "https://github.com/Fector101/android_notify/" for Documentation.') - return + print( + 'This Package Only Runs on Android !!! ---> Check "https://github.com/Fector101/android_notify/" for Documentation.') + return None - asks_permission_if_needed(no_androidx=True) - channel_id=channel_name.replace(' ','_').lower().lower() if not channel_id else channel_id + asks_permission_if_needed(legacy=True) + channel_id = channel_name.replace(' ', '_').lower().lower() if not channel_id else channel_id # Get notification manager notification_manager = context.getSystemService(context.NOTIFICATION_SERVICE) # importance= autoclass('android.app.NotificationManager').IMPORTANCE_HIGH # also works #NotificationManager.IMPORTANCE_DEFAULT - importance= NotificationManagerCompat.IMPORTANCE_HIGH #autoclass('android.app.NotificationManager').IMPORTANCE_HIGH also works #NotificationManager.IMPORTANCE_DEFAULT + importance = NotificationManagerCompat.IMPORTANCE_HIGH # autoclass('android.app.NotificationManager').IMPORTANCE_HIGH also works #NotificationManager.IMPORTANCE_DEFAULT # Notification Channel (Required for Android 8.0+) if BuildVersion.SDK_INT >= 26: - channel = NotificationChannel(channel_id, channel_name,importance) + channel = NotificationChannel(channel_id, channel_name, importance) notification_manager.createNotificationChannel(channel) # Build the notification builder = NotificationCompatBuilder(context, channel_id) builder.setContentTitle(title) builder.setContentText(message) - insert_app_icon(builder,custom_app_icon_path) + insert_app_icon(builder, custom_app_icon_path) builder.setDefaults(Notification.DEFAULT_ALL) builder.setPriority(Notification.PRIORITY_HIGH) if img_path: - print('android_notify- img_path arg deprecated use "large_icon_path or big_picture_path or custom_app_icon_path" instead') + print( + 'android_notify- img_path arg deprecated use "large_icon_path or big_picture_path or custom_app_icon_path" instead') if style: - print('android_notify- "style" arg deprecated use args "big_picture_path", "large_icon_path", "big_text", "lines" instead') + print( + 'android_notify- "style" arg deprecated use args "big_picture_path", "large_icon_path", "big_text", "lines" instead') big_picture = None if big_picture_path: try: big_picture = get_image_uri(big_picture_path) except FileNotFoundError as e: - print('android_notify- Error Getting Uri for big_picture_path: ',e) + print('android_notify- Error Getting Uri for big_picture_path: ', e) large_icon = None if large_icon_path: try: large_icon = get_image_uri(large_icon_path) except FileNotFoundError as e: - print('android_notify- Error Getting Uri for large_icon_path: ',e) - + print('android_notify- Error Getting Uri for large_icon_path: ', e) # Apply notification styles try: @@ -212,9 +229,8 @@ def send_notification( builder.setStyle(big_picture_style) except Exception as e: - print('android_notify- Error Failed Adding Style: ',e) + print('android_notify- Error Failed Adding Style: ', e) # Display the notification notification_id = random.randint(0, 100) notification_manager.notify(notification_id, builder.build()) return notification_id - diff --git a/android_notify/sword.py b/android_notify/sword.py index d03b702..eb880e3 100644 --- a/android_notify/sword.py +++ b/android_notify/sword.py @@ -6,27 +6,29 @@ from .an_types import Importance from .an_utils import can_accept_arguments, get_python_activity_context, \ get_android_importance, generate_channel_id, get_img_from_path, setLayoutText, \ - get_bitmap_from_url, add_data_to_intent, get_sound_uri, icon_finder, get_bitmap_from_path, can_show_permission_request_popup, open_settings_screen + get_bitmap_from_url, add_data_to_intent, get_sound_uri, icon_finder, get_bitmap_from_path, \ + can_show_permission_request_popup, open_settings_screen - -from .config import from_service_file, get_python_activity,get_notification_manager,ON_ANDROID,on_flet_app +from .config import from_service_file, get_python_activity, get_notification_manager, ON_ANDROID, on_flet_app from .config import (Bundle, String, BuildVersion, - Intent,PendingIntent, + Intent, PendingIntent, app_storage_path, - NotificationChannel,RemoteViews, + NotificationChannel, RemoteViews, run_on_ui_thread, ) from .config import (AndroidNotification, NotificationCompatBuilder, - NotificationCompatBigTextStyle,NotificationCompatBigPictureStyle, + NotificationCompatBigTextStyle, NotificationCompatBigPictureStyle, NotificationCompatInboxStyle, Color, Manifest ) from .styles import NotificationStyles from .base import BaseNotification -DEV=0 + +DEV = 0 PythonActivity = get_python_activity() context = get_python_activity_context() + class Notification(BaseNotification): """ Send a notification on Android. @@ -60,17 +62,18 @@ class Notification(BaseNotification): """ notification_ids = [0] - btns_box={} - main_functions={} - passed_check=False + btns_box = {} + main_functions = {} + passed_check = False # During Development (When running on PC) BaseNotification.logs = not ON_ANDROID - def __init__(self,**kwargs): #@dataclass already does work + + def __init__(self, **kwargs): # @dataclass already does work super().__init__(**kwargs) - self.__id = self.id or self.__get_unique_id() # Different use from self.name all notifications require `integers` id's not `strings` - self.id = self.__id # To use same Notification in different instances + self.__id = self.id or self.__get_unique_id() # Different use from self.name all notifications require `integers` id's not `strings` + self.id = self.__id # To use same Notification in different instances # To Track progressbar last update (According to Android Docs Don't update bar to often, I also faced so issues when doing that) self.__update_timer = None @@ -78,17 +81,17 @@ def __init__(self,**kwargs): #@dataclass already does work self.__progress_bar_title = '' self.__cooldown = 0 - self.__built_parameter_filled=False - self.__using_set_priority_method=False + self.__built_parameter_filled = False + self.__using_set_priority_method = False # For components self.__lines = [] - self.__has_small_icon = False #important notification can't send without + self.__has_small_icon = False # important notification can't send without self.__using_custom = self.message_color or self.title_color self.__format_channel(self.channel_name, self.channel_id) - self.__builder = None # want to make builder always available for getter + self.__builder = None # want to make builder always available for getter self.notification_manager = None - + if not ON_ANDROID: return @@ -98,10 +101,10 @@ def __init__(self,**kwargs): #@dataclass already does work self.notification_manager = get_notification_manager() self.__builder = NotificationCompatBuilder(context, self.channel_id) - def addLine(self,text:str): + def addLine(self, text: str): self.__lines.append(text) - def cancel(self,_id=0): + def cancel(self, _id=0): """ Removes a Notification instance from tray :param _id: not required uses Notification instance id as default @@ -128,13 +131,13 @@ def channelExists(cls, channel_id): """ if not ON_ANDROID: return False - notification_manager= get_notification_manager() + notification_manager = get_notification_manager() if BuildVersion.SDK_INT >= 26 and notification_manager.getNotificationChannel(channel_id): return True return False - + @classmethod - def createChannel(cls, id, name:str, description='',importance:Importance='urgent',res_sound_name=None): + def createChannel(cls, id, name: str, description='', importance: Importance = 'urgent', res_sound_name=None): """ Creates a user visible toggle button for specific notifications, Required For Android 8.0+ :param id: Used to send other notifications later through same channel. @@ -148,7 +151,7 @@ def createChannel(cls, id, name:str, description='',importance:Importance='urgen if not ON_ANDROID: return False - notification_manager= get_notification_manager() + notification_manager = get_notification_manager() android_importance_value = get_android_importance(importance) sound_uri = get_sound_uri(res_sound_name) @@ -170,7 +173,7 @@ def deleteChannel(cls, channel_id): if cls.channelExists(channel_id): get_notification_manager().deleteNotificationChannel(channel_id) - + @classmethod def deleteAllChannel(cls): """Deletes all notification channel @@ -189,9 +192,9 @@ def deleteAllChannel(cls): channel_id = channel.getId() notification_manager.deleteNotificationChannel(channel_id) return amount - + @classmethod - def doChannelsExist(cls,ids): + def doChannelsExist(cls, ids): """Uses list of IDs to check if channel exists returns list of channels that don't exist """ @@ -201,13 +204,12 @@ def doChannelsExist(cls,ids): notification_manager = get_notification_manager() for channel_id in ids: exists = ( - BuildVersion.SDK_INT >= 26 and - notification_manager.getNotificationChannel(channel_id) + BuildVersion.SDK_INT >= 26 and + notification_manager.getNotificationChannel(channel_id) ) if not exists: missing_channels.append(channel_id) return missing_channels - def refresh(self): """TO apply new components on notification""" @@ -217,7 +219,7 @@ def refresh(self): self.__applyNewLinesIfAny() self.__dispatch_notification() - def setBigPicture(self,path): + def setBigPicture(self, path): """ set a Big Picture at the bottom :param path: can be `Relative Path` or `URL` @@ -229,20 +231,20 @@ def setBigPicture(self,path): # When on android there are other logs print('Done setting big picture') - def setSmallIcon(self,path): + def setSmallIcon(self, path): """ sets small icon to the top left :param path: can be `Relative Path` or `URL` :return: """ if ON_ANDROID: - self.app_icon=path + self.app_icon = path self.__insert_app_icon(path) if self.logs: # When on android there are other logs print('Done setting small icon') - def setLargeIcon(self,path): + def setLargeIcon(self, path): """ sets Large icon to the right :param path: can be `Relative Path` or `URL` @@ -251,10 +253,10 @@ def setLargeIcon(self,path): if ON_ANDROID: self.__build_img(path, NotificationStyles.LARGE_ICON) elif self.logs: - #When on android there are other logs + # When on android there are other logs print('Done setting large icon') - def setBigText(self,body,title="",summary=""): + def setBigText(self, body, title="", summary=""): """Sets a big text for when drop down button is pressed :param body: The big text that will be displayed @@ -267,7 +269,7 @@ def setBigText(self,body,title="",summary=""): big_text_style.setBigContentTitle(str(title)) if summary: big_text_style.setSummaryText(str(summary)) - + big_text_style.bigText(str(body)) self.__builder.setStyle(big_text_style) elif self.logs: @@ -287,7 +289,7 @@ def setSubText(self, text): if ON_ANDROID: self.__builder.setSubText(self.sub_text) - def setColor(self,color:str): + def setColor(self, color: str): """ Sets Notification accent color, visible change in SmallIcon color :param color: str - red,pink,... (to be safe use hex code) @@ -311,9 +313,9 @@ def setWhen(self, secs_ago): ----- - Android expects the `when` timestamp in **milliseconds** since the Unix epoch. """ - + if ON_ANDROID: - ms=int((time.time() - secs_ago) * 1000) + ms = int((time.time() - secs_ago) * 1000) self.__builder.setWhen(ms) self.__builder.setShowWhen(True) if self.logs: @@ -326,16 +328,16 @@ def showInfiniteProgressBar(self): if self.logs: print('Showing infinite progressbar') if ON_ANDROID: - self.__builder.setProgress(0,0, True) + self.__builder.setProgress(0, 0, True) self.refresh() - def updateTitle(self,new_title): + def updateTitle(self, new_title): """Changes Old Title Args: new_title (str): New Notification Title """ - self.title=str(new_title) + self.title = str(new_title) if self.logs: print(f'new notification title: {self.title}') if ON_ANDROID: @@ -345,13 +347,13 @@ def updateTitle(self,new_title): self.__builder.setContentTitle(String(self.title)) self.refresh() - def updateMessage(self,new_message): + def updateMessage(self, new_message): """Changes Old Message Args: new_message (str): New Notification Message """ - self.message=str(new_message) + self.message = str(new_message) if self.logs: print(f'new notification message: {self.message}') if ON_ANDROID: @@ -361,7 +363,8 @@ def updateMessage(self,new_message): self.__builder.setContentText(String(self.message)) self.refresh() - def updateProgressBar(self, current_value:int, message:str='', title:str='', cooldown=0.5, _callback:Callable=None): + def updateProgressBar(self, current_value: int, message: str = '', title: str = '', cooldown=0.5, + _callback: Callable = None): """Updates progress bar current value Args: @@ -387,7 +390,7 @@ def updateProgressBar(self, current_value:int, message:str='', title:str='', coo return def delayed_update(): - if self.__update_timer is None: # Ensure we are not executing an old timer + if self.__update_timer is None: # Ensure we are not executing an old timer if self.logs: print('ProgressBar update skipped: bar has been removed.') return @@ -415,14 +418,14 @@ def delayed_update(): self.refresh() self.__update_timer = None - # Start a new timer that runs after 0.5 seconds # self.__timer_start_time = time.time() # for logs self.__cooldown = cooldown self.__update_timer = threading.Timer(cooldown, delayed_update) self.__update_timer.start() - def removeProgressBar(self, message='', show_on_update=True, title:str='', cooldown=0.5, _callback:Callable=None): + def removeProgressBar(self, message='', show_on_update=True, title: str = '', cooldown=0.5, + _callback: Callable = None): """Removes Progress Bar from Notification Args: @@ -448,7 +451,7 @@ def removeProgressBar(self, message='', show_on_update=True, title:str='', coold def delayed_update(): if self.logs: msg = message or self.message - title_=title or self.title + title_ = title or self.title print(f'removed progress bar with message: {msg} and title: {title_}') if _callback: @@ -472,19 +475,19 @@ def delayed_update(): # Incase `self.updateProgressBar delayed_update` is called right before this method, so android doesn't bounce update threading.Timer(cooldown, delayed_update).start() - def setPriority(self,importance:Importance): + def setPriority(self, importance: Importance): """ For devices less than android 8 :param importance: ['urgent', 'high', 'medium', 'low', 'none'] defaults to 'urgent' i.e. makes a sound and shows briefly :return: """ - self.__using_set_priority_method=True + self.__using_set_priority_method = True if ON_ANDROID: android_importance_value = get_android_importance(importance) if not isinstance(android_importance_value, str): # Can be an empty str if importance='none' self.__builder.setPriority(android_importance_value) - def send(self,silent:bool=False,persistent=False,close_on_click=True): + def send(self, silent: bool = False, persistent=False, close_on_click=True): """Sends notification Args: @@ -498,22 +501,22 @@ def send(self,silent:bool=False,persistent=False,close_on_click=True): self.__dispatch_notification() self.__send_logs() - - def send_(self,silent:bool=False,persistent=False,close_on_click=True): + + def send_(self, silent: bool = False, persistent=False, close_on_click=True): """Sends notification without checking for additional notification permission Args: silent (bool): True if you don't want to show briefly on screen persistent (bool): True To not remove Notification When User hits clears All notifications button close_on_click (bool): True if you want Notification to be removed when clicked - """ + """ self.passed_check = True - self.send(silent,persistent,close_on_click) + self.send(silent, persistent, close_on_click) def __send_logs(self): if not self.logs: return - string_to_display='' + string_to_display = '' print("\n Sent Notification!!!") displayed_args = [ "title", "message", @@ -532,14 +535,14 @@ def __send_logs(self): else: string_to_display += f'\n {name}: {value}' - string_to_display +="\n (Won't Print Logs When Complied,except if selected `Notification.logs=True`)" + string_to_display += "\n (Won't Print Logs When Complied,except if selected `Notification.logs=True`)" print(string_to_display) - + @property def builder(self): return self.__builder - - def addButton(self, text:str,on_release): + + def addButton(self, text: str, on_release): """For adding action buttons Args: @@ -552,8 +555,8 @@ def addButton(self, text:str,on_release): if not ON_ANDROID: return - action = f"{text}_{self.id}" # tagging with id so i can find specified notification in my object - + action = f"{text}_{self.id}" # tagging with id so i can find specified notification in my object + action_intent = Intent(context, PythonActivity) action_intent.setAction(action) action_intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) @@ -567,7 +570,7 @@ def addButton(self, text:str,on_release): # action_intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP) if self.logs: - print('Button action: ',action) + print('Button action: ', action) pending_action_intent = PendingIntent.getActivity( context, 0, @@ -577,13 +580,11 @@ def addButton(self, text:str,on_release): # Convert text to CharSequence action_text = cast('java.lang.CharSequence', String(text)) - - # Add action with proper types self.__builder.addAction( int(context.getApplicationInfo().icon), # Cast icon to int - action_text, # CharSequence text - pending_action_intent # PendingIntent + action_text, # CharSequence text + pending_action_intent # PendingIntent ) # Set content intent for notification tap self.__builder.setContentIntent(pending_action_intent) @@ -598,9 +599,10 @@ def removeButtons(self): print('Removed Notification Buttons') @run_on_ui_thread - def addNotificationStyle(self,style:str,already_sent=False): + def addNotificationStyle(self, style: str, already_sent=False): """Adds Style to Notification - Version 1.51.2+ Exposes method to Users (Note): Always try to Call On UI Thread + + Note: This method has Deprecated Use - (setLargeIcon, setBigPicture, setBigText and setLines) Instead Args: style (str): required style @@ -646,31 +648,31 @@ def setLines(self, lines: list): if self.logs: print('Added Lines: ', lines) - def setSound(self,res_sound_name): + def setSound(self, res_sound_name): """ Sets sound for devices less than android 8 (For 8+ use createChannel) :param res_sound_name: audio file file name (without .wav or .mp3) locate in res/raw/ """ - + if not ON_ANDROID: return - + if res_sound_name and BuildVersion.SDK_INT < 26: try: self.__builder.setSound(get_sound_uri(res_sound_name)) except Exception as failed_adding_sound_device_below_android8: - print("failed_adding_sound_device_below_android8:",failed_adding_sound_device_below_android8) + print("failed_adding_sound_device_below_android8:", failed_adding_sound_device_below_android8) traceback.print_exc() - + def __dispatch_notification(self): # self.passed_check is for self.send_() some devices don't return true when checking for permission when it's actually True in settingsAdd commentMore actions # And so users can do Notification.passed_check = True at top of their file and use regular .send() - - if from_service_file(): # android has_permission has some internal error when checking from service + + if from_service_file(): # android has_permission has some internal error when checking from service try: self.notification_manager.notify(self.__id, self.__builder.build()) except Exception as sending_notification_from_service_error: - print('error sending notification from service:',sending_notification_from_service_error) + print('error sending notification from service:', sending_notification_from_service_error) elif on_flet_app() or self.passed_check or NotificationHandler.has_permission(): try: self.notification_manager.notify(self.__id, self.__builder.build()) @@ -683,22 +685,22 @@ def __dispatch_notification(self): # Not asking for permission too frequently, This makes dialog popup to stop showing # NotificationHandler.asks_permission() - def start_building(self, persistent=False,close_on_click=True,silent:bool=False): + def start_building(self, persistent=False, close_on_click=True, silent: bool = False): # Main use is for foreground service, bypassing .notify in .send method to let service.startForeground(...) send it self.silent = silent or self.silent if not ON_ANDROID: - return NotificationCompatBuilder # this is just a facade + return NotificationCompatBuilder # this is just a facade self.__create_basic_notification(persistent, close_on_click) - if self.style not in ['simple','']: + if self.style not in ['simple', '']: self.addNotificationStyle(self.style) self.__applyNewLinesIfAny() - + return self.__builder def __applyNewLinesIfAny(self): if self.__lines: self.setLines(self.__lines) - self.__lines=[] # for refresh method to known when new lines added + self.__lines = [] # for refresh method to known when new lines added def __create_basic_notification(self, persistent, close_on_click): if not self.channelExists(self.channel_id): @@ -721,13 +723,13 @@ def __create_basic_notification(self, persistent, close_on_click): try: self.__add_intent_to_open_app() except Exception as failed_to_add_intent_to_open_app: - print('failed_to_add_intent_to_open_app Error: ',failed_to_add_intent_to_open_app) + print('failed_to_add_intent_to_open_app Error: ', failed_to_add_intent_to_open_app) traceback.print_exc() self.__built_parameter_filled = True - def __insert_app_icon(self,path=''): - if BuildVersion.SDK_INT >= 23 and (path or self.app_icon not in ['','Defaults to package app icon']): + def __insert_app_icon(self, path=''): + if BuildVersion.SDK_INT >= 23 and (path or self.app_icon not in ['', 'Defaults to package app icon']): # Bitmap Insert as Icon Not available below Android 6 if self.logs: print('getting custom icon...') @@ -740,9 +742,9 @@ def set_default_icon(): fallback_icon_path = None if on_flet_app(): - fallback_icon_path=icon_finder("flet-appicon.png") + fallback_icon_path = icon_finder("flet-appicon.png") elif "ru.iiec.pydroid3" in os.path.dirname(os.path.abspath(__file__)): - fallback_icon_path=icon_finder("pydroid3-appicon.png") + fallback_icon_path = icon_finder("pydroid3-appicon.png") else: set_default_icon() @@ -751,29 +753,29 @@ def set_default_icon(): if not success: print("error_using_fallback_appicon") set_default_icon() - + self.__has_small_icon = True - - def __set_smallicon_with_bitmap_from_path(self,fullpath): + def __set_smallicon_with_bitmap_from_path(self, fullpath): try: bitmap = get_bitmap_from_path(fullpath) if bitmap: self.__set_builder_icon_with_bitmap(bitmap) return True except Exception as error_using_bitmap_for_appicon: - print("error_using_bitmap_for_appicon :",error_using_bitmap_for_appicon) + print("error_using_bitmap_for_appicon :", error_using_bitmap_for_appicon) traceback.print_exc() return False def __build_img(self, user_img, img_style): if user_img.startswith('http://') or user_img.startswith('https://'): def callback(bitmap_): - self.__apply_notification_image(bitmap_,img_style) + self.__apply_notification_image(bitmap_, img_style) + thread = threading.Thread( - target=get_bitmap_from_url, - args=[user_img,callback,self.logs] - ) + target=get_bitmap_from_url, + args=[user_img, callback, self.logs] + ) thread.start() else: bitmap = get_img_from_path(user_img) @@ -782,7 +784,7 @@ def callback(bitmap_): def __set_icon_from_bitmap(self, img_path): """Path can be a link or relative path""" - + if img_path.startswith('http://') or img_path.startswith('https://'): def callback(bitmap_): if bitmap_: @@ -792,27 +794,30 @@ def callback(bitmap_): print('Using Default Icon as fallback......') self.__builder.setSmallIcon(context.getApplicationInfo().icon) self.__has_small_icon = True + threading.Thread( target=get_bitmap_from_url, - args=[img_path,callback,self.logs] - ).start() + args=[img_path, callback, self.logs] + ).start() else: - bitmap = get_img_from_path(img_path) #get_img_from_path is different from get_bitmap_from_path because it those some logging for user + bitmap = get_img_from_path( + img_path) # get_img_from_path is different from get_bitmap_from_path because it those some logging for user if bitmap: self.__set_builder_icon_with_bitmap(bitmap) else: if self.logs: - app_folder=os.path.join(app_storage_path(),'app') + app_folder = os.path.join(app_storage_path(), 'app') img_absolute_path = os.path.join(app_folder, img_path) - print(f'Failed getting bitmap for custom notification icon defaulting to app icon\n absolute path {img_absolute_path}') + print( + f'Failed getting bitmap for custom notification icon defaulting to app icon\n absolute path {img_absolute_path}') self.__builder.setSmallIcon(context.getApplicationInfo().icon) self.__has_small_icon = True - - def __set_builder_icon_with_bitmap(self,bitmap): + + def __set_builder_icon_with_bitmap(self, bitmap): try: Icon = autoclass('android.graphics.drawable.Icon') except Exception as autoclass_icon_error: - print("Couldn't find class to set custom icon:",autoclass_icon_error) + print("Couldn't find class to set custom icon:", autoclass_icon_error) self.__builder.setSmallIcon(context.getApplicationInfo().icon) self.__has_small_icon = True return @@ -836,28 +841,33 @@ def __apply_notification_image(self, bitmap, img_style): print('Done adding image to notification-------') except Exception as notification_image_error: img = self.large_icon_path if img_style == NotificationStyles.LARGE_ICON else self.big_picture_path - print(f'Failed adding Image of style: {img_style} || From path: {img}, Exception {notification_image_error}') - print('could not get Img traceback: ',traceback.format_exc()) + print( + f'Failed adding Image of style: {img_style} || From path: {img}, Exception {notification_image_error}') + print('could not get Img traceback: ', traceback.format_exc()) def __add_intent_to_open_app(self): intent = Intent(context, PythonActivity) intent.setFlags( - Intent.FLAG_ACTIVITY_CLEAR_TOP | # Makes Sure tapping notification always brings the existing instance of app forward. - Intent.FLAG_ACTIVITY_SINGLE_TOP | # If the activity is already at the top, reuse it instead of creating a new instance. - Intent.FLAG_ACTIVITY_NEW_TASK # Required when starting an Activity from a Service; ignored when starting from another Activity. + Intent.FLAG_ACTIVITY_CLEAR_TOP | # Makes Sure tapping notification always brings the existing instance of app forward. + Intent.FLAG_ACTIVITY_SINGLE_TOP | # If the activity is already at the top, reuse it instead of creating a new instance. + Intent.FLAG_ACTIVITY_NEW_TASK + # Required when starting an Activity from a Service; ignored when starting from another Activity. ) action = str(self.name or self.__id) intent.setAction(action) - add_data_to_intent(intent,self.title) - self.main_functions[action]=self.callback + add_data_to_intent(intent, self.title) + self.main_functions[action] = self.callback + + #intent.setAction(Intent.ACTION_MAIN) # Marks this intent as the main entry point of the app, like launching from the home screen. + #intent.addCategory(Intent.CATEGORY_LAUNCHER) # Adds the launcher category so Android treats it as a launcher app intent and properly manages the task/back stack. pending_intent = PendingIntent.getActivity( - context, 0, - intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - ) + context, 0, + intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ) self.__builder.setContentIntent(pending_intent) - def __format_channel(self, channel_name:str='Default Channel',channel_id:str='default_channel'): + def __format_channel(self, channel_name: str = 'Default Channel', channel_id: str = 'default_channel'): """ Formats and sets self.channel_name and self.channel_id to a formatted version :param channel_name: @@ -892,7 +902,8 @@ def getChannels(cls) -> list[Any] | Any: return get_notification_manager().getNotificationChannels() def __apply_basic_custom_style(self): - NotificationCompatDecoratedCustomViewStyle = autoclass('androidx.core.app.NotificationCompat$DecoratedCustomViewStyle') + NotificationCompatDecoratedCustomViewStyle = autoclass( + 'androidx.core.app.NotificationCompat$DecoratedCustomViewStyle') # Load layout resources = context.getResources() @@ -909,20 +920,20 @@ def __apply_basic_custom_style(self): notificationLayoutExpanded = RemoteViews(package_name, large_layout_id) if DEV: - print('small: ',small_layout_id, 'notificationLayout: ',notificationLayout) + print('small: ', small_layout_id, 'notificationLayout: ', notificationLayout) # Notification Content setLayoutText( - layout=notificationLayout, id=title_id, - text=self.title, color=self.title_color + layout=notificationLayout, id=title_id, + text=self.title, color=self.title_color ) setLayoutText( - layout=notificationLayoutExpanded, id=title_id, - text=self.title, color=self.title_color + layout=notificationLayoutExpanded, id=title_id, + text=self.title, color=self.title_color ) setLayoutText( - layout=notificationLayoutExpanded, id=message_id, - text=self.message, color=self.message_color + layout=notificationLayoutExpanded, id=message_id, + text=self.message, color=self.message_color ) # self.__setLayoutText( # layout=notificationLayout, id=message_id, @@ -946,11 +957,12 @@ class NotificationHandler: """For Notification Operations """ __name = None __bound = False - __requesting_permission=False - android_activity=None + __requesting_permission = False + android_activity = None if ON_ANDROID and not on_flet_app(): from android import activity android_activity = activity + @classmethod def get_name(cls): """Returns name or id str for Clicked Notification.""" @@ -958,7 +970,7 @@ def get_name(cls): return "Not on Android" saved_intent = cls.__name - cls.__name = None # so value won't be set when opening app not from notification + cls.__name = None # so value won't be set when opening app not from notification # print('saved_intent ',saved_intent) # if not saved_intent or (isinstance(saved_intent, str) and saved_intent.startswith("android.intent")): # All other notifications are not None after First notification opens app @@ -985,31 +997,32 @@ def __notification_handler(cls, intent): """ if not cls.is_on_android(): return "Not on Android" - buttons_object=Notification.btns_box - notifty_functions=Notification.main_functions + #print('intent.getStringExtra("title")',intent.getStringExtra("title")) + buttons_object = Notification.btns_box + notifty_functions = Notification.main_functions if DEV: - print("notifty_functions ",notifty_functions) + print("notifty_functions ", notifty_functions) print("buttons_object", buttons_object) try: action = intent.getAction() cls.__name = action # print("The Action --> ",action) - if action == "android.intent.action.MAIN": # Not Open From Notification + if action == "android.intent.action.MAIN": # Not Open From Notification cls.__name = None return 'Not notification' - print(intent.getStringExtra("title")) + # print(intent.getStringExtra("title")) try: if action in notifty_functions and notifty_functions[action]: notifty_functions[action]() elif action in buttons_object: buttons_object[action]() except Exception as notification_handler_function_error: - print("Error Type ",notification_handler_function_error) + print("Error Type ", notification_handler_function_error) print('Failed to run function: ', traceback.format_exc()) except Exception as extracting_notification_props_error: - print('Notify Handler Failed ',extracting_notification_props_error) + print('Notify Handler Failed ', extracting_notification_props_error) @classmethod def bindNotifyListener(cls): @@ -1019,7 +1032,7 @@ def bindNotifyListener(cls): if not cls.is_on_android(): return "Not on Android" - #TODO keep trying BroadcastReceiver + # TODO keep trying BroadcastReceiver if cls.__bound: print("binding done already ") return True @@ -1028,7 +1041,7 @@ def bindNotifyListener(cls): cls.__bound = True return True except Exception as binding_listener_error: - print('Failed to bin notifications listener',binding_listener_error) + print('Failed to bin notifications listener', binding_listener_error) return False @classmethod @@ -1037,14 +1050,14 @@ def unbindNotifyListener(cls): if not cls.is_on_android(): return "Not on Android" - #Beta TODO use BroadcastReceiver + # Beta TODO use BroadcastReceiver if on_flet_app() or from_service_file(): - return False # error 'NoneType' object has no attribute 'registerNewIntentListener' + return False # error 'NoneType' object has no attribute 'registerNewIntentListener' try: cls.android_activity.unbind(on_new_intent=cls.__notification_handler) return True except Exception as unbinding_listener_error: - print("Failed to unbind notifications listener: ",unbinding_listener_error) + print("Failed to unbind notifications listener: ", unbinding_listener_error) return False @staticmethod @@ -1061,9 +1074,9 @@ def has_permission(): if not ON_ANDROID: return True - if BuildVersion.SDK_INT < 33: # Android 12 below + if BuildVersion.SDK_INT < 33: # Android 12 below print("android_notify- On android 12 or less don't need permission") - return True + return True if on_flet_app(): Manifest = autoclass('android.Manifest$permission') @@ -1077,18 +1090,23 @@ def has_permission(): @classmethod @run_on_ui_thread - def asks_permission(cls,callback=None): + def asks_permission(cls, callback=None): """ Ask for permission to send notifications if needed. Passes True to callback if access granted """ + if not ON_ANDROID: + print("android_notify- Can't ask permission when not on android") + return None + if cls.__requesting_permission: + print("android_notify- still requesting permission ") return True - if BuildVersion.SDK_INT < 33: # Android 12 below + if BuildVersion.SDK_INT < 33: # Android 12 below print("android_notify- On android 12 or less don't need permission") - - if not ON_ANDROID or BuildVersion.SDK_INT < 33: # Android 12 below: + + if not ON_ANDROID or BuildVersion.SDK_INT < 33: # Android 12 below: try: if callback: if can_accept_arguments(callback, True): @@ -1096,16 +1114,17 @@ def asks_permission(cls,callback=None): else: callback() except Exception as request_permission_error: - print('Exception: ',request_permission_error) - print('Permission response callback error: ',traceback.format_exc()) + print('Exception: ', request_permission_error) + print('Permission response callback error: ', traceback.format_exc()) return if not can_show_permission_request_popup(): - print("""android_notify- Permission to send notifications has been denied permanently. Please enable it from settings. - This happens when the user denies permission twice from the popup.""") + print("""android_notify- Permission to send notifications has been denied permanently. +This happens when the user denies permission twice from the popup. +Opening notification settings...""") open_settings_screen() - return + return None def on_permissions_result(permissions, grants): try: @@ -1115,8 +1134,8 @@ def on_permissions_result(permissions, grants): else: callback() except Exception as request_permission_error: - print('Exception: ',request_permission_error) - print('Permission response callback error: ',traceback.format_exc()) + print('Exception: ', request_permission_error) + print('Permission response callback error: ', traceback.format_exc()) finally: cls.__requesting_permission = False @@ -1129,12 +1148,12 @@ def on_permissions_result(permissions, grants): else: from android.permissions import request_permissions, Permission cls.__requesting_permission = True - request_permissions([Permission.POST_NOTIFICATIONS],on_permissions_result) + request_permissions([Permission.POST_NOTIFICATIONS], on_permissions_result) return None else: cls.__requesting_permission = False if callback: - if can_accept_arguments(callback,True): + if can_accept_arguments(callback, True): callback(True) else: callback() @@ -1148,6 +1167,5 @@ def on_permissions_result(permissions, grants): NotificationHandler.bindNotifyListener() except Exception as bind_error: # error 'NoneType' object has no attribute 'registerNewIntentListener' - print("notification listener bind error:",bind_error) + print("notification listener bind error:", bind_error) traceback.print_exc() -