├── .gitignore ├── LICENSE ├── README.md └── src ├── misc ├── AndroidManifest.tmpl.xml └── build.tmpl.gradle └── python ├── buildozer.spec ├── libs ├── ContextHolder.java ├── KivyFirebaseMessagingBackgroundExecutor.java ├── KivyFirebaseMessagingBackgroundService.java ├── KivyFirebaseMessagingReceiver.java ├── KivyFirebaseMessagingService.java ├── KivyFirebaseMessagingStore.java ├── KivyFirebaseMessagingUtils.java └── PlatformIntermediate.java ├── main.py ├── pushyy ├── __init__.py ├── pushyy.py └── remote_message.py └── python_notification_handler.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | # *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .buildozer/ 132 | bin/ 133 | libs/ 134 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Thomas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pushyy 2 | A Python module meant to simplify working with push notifications in Kivy 3 | 4 | **Video tutorial**: https://youtu.be/8nrXsWeRG8I 5 | 6 | Features 7 | -------------- 8 | - Receive push notifications when your app is in the **foreground** 9 | - Receive push notifications when your app is in the **background** or not running 10 | - Run a background function when notification is received when app is not running 11 | - Get a device token 12 | - Listen for device token changes 13 | 14 | Credits 15 | -------------- 16 | - [Flutter Firebase Messaging](https://github.com/FirebaseExtended/flutterfire/tree/master/packages/firebase_messaging/firebase_messaging): The Java classes are from there 17 | 18 | - Kindly create a PR correcting the copyright if it's wrong :) 19 | - [Electrum](https://github.com/spesmilo/electrum/tree/master/electrum) for the [onNewIntent](https://github.com/spesmilo/electrum/blob/6650e6bbae12a79e12667857ee039f1b1f30c7e3/electrum/gui/kivy/main_window.py#L620) 20 | 21 | Usage Overview 22 | -------------- 23 | ```python 24 | from pushyy import Pushyy 25 | 26 | # Get device token 27 | def my_token_callback(token: str) -> None: 28 | send_to_server(token) 29 | 30 | Pushyy().get_device_token(my_token_callback) 31 | 32 | # Listen for new device token 33 | def new_token_callback(token: str) -> None: 34 | print(token) 35 | 36 | Pushyy().token_change_listener(new_token_callback) 37 | 38 | # Get notification data when app is in foreground 39 | def my_foreground_message_callback(notification_data: RemoteMessage) -> None: 40 | print(notification_data) 41 | 42 | Pushyy().foreground_message_handler(my_foreground_message_callback) 43 | 44 | # Get notification data when user taps on notification from tray 45 | def my_notification_click_callback(notification_data: RemoteMessage) -> None: 46 | print(notification_data) 47 | 48 | Pushyy().notification_click_handler(my_notification_click_callback) 49 | 50 | ``` 51 | > See `src/python/main.py` on how the UI is being updated 52 | 53 | ##### Background function 54 | To run custom code in the background when a notification is received and your application is not running, write your code in the ```my_background_callback``` function in [python_notification_handler.py](src/python/python_notification_handler.py) 55 | ```python 56 | def my_background_callback(notification_data: RemoteMessage) -> None: 57 | """ 58 | Note: Application is not visible to the user here 59 | One of the things you can do here: Mark a chat message 60 | as delivered by making a request to your server. 61 | """ 62 | try: 63 | # connect to server 64 | pass 65 | except: 66 | pass 67 | ``` 68 | 69 | Set up 70 | -------------- 71 | ##### Part 1 72 | 1. Clone [python-for-android](https://github.com/kivy/python-for-android) 73 | 2. Open the file `pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle` 74 | 3. Add the following: 75 | - Under `buildscript->dependencies` add `classpath 'com.google.gms:google-services:4.3.4'` 76 | - Below `apply plugin: 'com.android.application'` add `apply plugin: 'com.google.gms.google-services'` 77 | - Under `dependencies` add `implementation platform('com.google.firebase:firebase-bom:X.Y.Z')` (replace XYZ with the latest version from [here](https://firebase.google.com/docs/android/learn-more#bom), tested with `28.1.0`) 78 | 4. Open the file `pythonforandroid/bootstraps/sdl2/build/templates/AndroidManifest.tmpl.xml` 79 | 5. Before the `` tag, add 80 | ```xml 81 | 85 | 87 | 88 | 89 | 90 | 91 | 95 | 96 | 97 | 98 | 99 | ``` 100 | 6. Create a Firebase project [here](https://console.firebase.google.com/) 101 | - Add an Android app and skip the steps since we already did that at 102 | - Download the `google-services.json` 103 | - Move it to `pythonforandroid/bo -> Noneotstraps/common/build/` folder 104 | ##### Part 2 105 | 1. Place [pushyy.py](src/python/pushyy.py) and [python_notification_handler.py](src/python/python_notification_handler.py) next to your `main.py` 106 | 2. Place [libs/](src/python/libs) in the same folder as `buildozer.spec` 107 | 3. In your `buildozer.spec` find and set: 108 | ```bash 109 | android.add_src = libs/ 110 | android.gradle_dependencies = com.google.firebase:firebase-messaging,com.google.firebase:firebase-analytics,com.google.code.gson:gson:2.8.6 111 | p4a.source_dir = /path/to/cloned/python-for-android 112 | 113 | services = PythonNotificationHandler:python_notification_handler.py 114 | # NB: File name must be python_notification_handler.py 115 | ``` 116 | 5. Open `PlatformIntermediate.java` from your `libs/` folder and replace `com.waterfall.youtube` with `your.app.packagename` 117 | 118 | 6. Open `KivyFirebaseMessagingBackgroundExecutor.java` from your `libs/` folder and replace `com.waterfall.youtube` with `your.app.packagename` 119 | 120 | Notes 121 | --------- 122 | - This module is aimed for Android. For iOS, you may consider [this](https://youtu.be/mONyhxt2KV8) video 123 | - Just to clarify, with [Plyer](https://github.com/kivy/plyer) you get to show local notifications, not push notifications. [Which notifications should i use, Push notification or local notification?](https://stackoverflow.com/questions/45343427/which-notifications-should-i-use-push-notification-or-local-notification) 124 | - Why Pushyy? First thing that came to mind `¯\_(ツ)_/¯` 125 | -------------------------------------------------------------------------------- /src/misc/AndroidManifest.tmpl.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | 11 | = 9 %} 17 | android:xlargeScreens="true" 18 | {% endif %} 19 | /> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for perm in args.permissions %} 30 | {% if '.' in perm %} 31 | 32 | {% else %} 33 | 34 | {% endif %} 35 | {% endfor %} 36 | 37 | {% if args.wakelock %} 38 | 39 | {% endif %} 40 | 41 | {% if args.billing_pubkey %} 42 | 43 | {% endif %} 44 | 45 | {{ args.extra_manifest_xml }} 46 | 47 | 48 | 57 | 67 | {% for l in args.android_used_libs %} 68 | 69 | {% endfor %} 70 | 71 | {% for m in args.meta_data %} 72 | {% endfor %} 73 | 74 | 75 | 83 | 84 | {% if args.launcher %} 85 | 86 | 87 | 88 | 89 | 90 | {% else %} 91 | 92 | 93 | 94 | 95 | {% endif %} 96 | 97 | {%- if args.intent_filters -%} 98 | {{- args.intent_filters -}} 99 | {%- endif -%} 100 | 101 | 102 | {% if args.launcher %} 103 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | {% endif %} 114 | 115 | {% if service or args.launcher %} 116 | 118 | {% endif %} 119 | {% for name in service_names %} 120 | 122 | {% endfor %} 123 | {% for name in native_services %} 124 | 125 | {% endfor %} 126 | 127 | {% if args.billing_pubkey %} 128 | 130 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | {% endif %} 139 | 143 | 145 | 146 | 147 | 148 | 149 | 153 | 154 | 155 | 156 | 157 | {% for a in args.add_activity %} 158 | 159 | {% endfor %} 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /src/misc/build.tmpl.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.5.2' 9 | classpath 'com.google.gms:google-services:4.3.4' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | jcenter() 17 | {%- for repo in args.gradle_repositories %} 18 | {{repo}} 19 | {%- endfor %} 20 | flatDir { 21 | dirs 'libs' 22 | } 23 | } 24 | } 25 | 26 | {% if is_library %} 27 | apply plugin: 'com.android.library' 28 | {% else %} 29 | apply plugin: 'com.android.application' 30 | apply plugin: 'com.google.gms.google-services' 31 | {% endif %} 32 | 33 | android { 34 | compileSdkVersion {{ android_api }} 35 | buildToolsVersion '{{ build_tools_version }}' 36 | defaultConfig { 37 | minSdkVersion {{ args.min_sdk_version }} 38 | targetSdkVersion {{ android_api }} 39 | versionCode {{ args.numeric_version }} 40 | versionName '{{ args.version }}' 41 | manifestPlaceholders = {{ args.manifest_placeholders}} 42 | } 43 | 44 | {% if debug_build -%} 45 | packagingOptions { 46 | doNotStrip '**/*.so' 47 | } 48 | {%- endif %} 49 | 50 | {% if args.sign -%} 51 | signingConfigs { 52 | release { 53 | storeFile file(System.getenv("P4A_RELEASE_KEYSTORE")) 54 | keyAlias System.getenv("P4A_RELEASE_KEYALIAS") 55 | storePassword System.getenv("P4A_RELEASE_KEYSTORE_PASSWD") 56 | keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD") 57 | } 58 | } 59 | 60 | {%- endif %} 61 | 62 | {% if args.packaging_options -%} 63 | packagingOptions { 64 | {%- for option in args.packaging_options %} 65 | {{option}} 66 | {%- endfor %} 67 | } 68 | {%- endif %} 69 | 70 | buildTypes { 71 | debug { 72 | } 73 | release { 74 | {% if args.sign -%} 75 | signingConfig signingConfigs.release 76 | {%- endif %} 77 | } 78 | } 79 | 80 | compileOptions { 81 | {% if args.enable_androidx %} 82 | sourceCompatibility JavaVersion.VERSION_1_8 83 | targetCompatibility JavaVersion.VERSION_1_8 84 | {% else %} 85 | sourceCompatibility JavaVersion.VERSION_1_7 86 | targetCompatibility JavaVersion.VERSION_1_7 87 | {% endif %} 88 | {%- for option in args.compile_options %} 89 | {{option}} 90 | {%- endfor %} 91 | } 92 | 93 | sourceSets { 94 | main { 95 | jniLibs.srcDir 'libs' 96 | java { 97 | 98 | {%- for adir, pattern in args.extra_source_dirs -%} 99 | srcDir '{{adir}}' 100 | {%- endfor -%} 101 | 102 | } 103 | } 104 | } 105 | 106 | } 107 | 108 | dependencies { 109 | implementation platform('com.google.firebase:firebase-bom:27.1.0') 110 | {%- for aar in aars %} 111 | implementation(name: '{{ aar }}', ext: 'aar') 112 | {%- endfor -%} 113 | {%- for jar in jars %} 114 | implementation files('src/main/libs/{{ jar }}') 115 | {%- endfor -%} 116 | {%- if args.depends -%} 117 | {%- for depend in args.depends %} 118 | implementation '{{ depend }}' 119 | {%- endfor %} 120 | {%- endif %} 121 | {% if args.presplash_lottie %} 122 | implementation 'com.airbnb.android:lottie:3.4.0' 123 | {%- endif %} 124 | } 125 | 126 | -------------------------------------------------------------------------------- /src/python/buildozer.spec: -------------------------------------------------------------------------------- 1 | [app] 2 | 3 | # (str) Title of your application 4 | title = My Application 5 | 6 | # (str) Package name 7 | package.name = youtube 8 | 9 | # (str) Package domain (needed for android/ios packaging) 10 | package.domain = com.waterfall 11 | 12 | # (str) Source code where the main.py live 13 | source.dir = . 14 | 15 | # (list) Source files to include (let empty to include all the files) 16 | source.include_exts = py,png,jpg,kv,atlas 17 | 18 | # (list) List of inclusions using pattern matching 19 | #source.include_patterns = assets/*,images/*.png 20 | 21 | # (list) Source files to exclude (let empty to not exclude anything) 22 | #source.exclude_exts = spec 23 | 24 | # (list) List of directory to exclude (let empty to not exclude anything) 25 | #source.exclude_dirs = tests, bin, venv 26 | 27 | # (list) List of exclusions using pattern matching 28 | #source.exclude_patterns = license,images/*/*.jpg 29 | 30 | # (str) Application versioning (method 1) 31 | version = 0.1 32 | 33 | # (str) Application versioning (method 2) 34 | # version.regex = __version__ = ['"](.*)['"] 35 | # version.filename = %(source.dir)s/main.py 36 | 37 | # (list) Application requirements 38 | # comma separated e.g. requirements = sqlite3,kivy 39 | requirements = python3,kivy,requests, urllib3, chardet, certifi, idna 40 | 41 | # (str) Custom source folders for requirements 42 | # Sets custom source for any requirements with recipes 43 | # requirements.source.kivy = ../../kivy 44 | 45 | # (str) Presplash of the application 46 | #presplash.filename = %(source.dir)s/data/presplash.png 47 | 48 | # (str) Icon of the application 49 | #icon.filename = %(source.dir)s/data/icon.png 50 | 51 | # (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) 52 | orientation = portrait 53 | 54 | # (list) List of service to declare 55 | services = PythonNotificationHandler:python_notification_handler.py 56 | 57 | # 58 | # OSX Specific 59 | # 60 | 61 | # 62 | # author = © Copyright Info 63 | 64 | # change the major version of python used by the app 65 | osx.python_version = 3 66 | 67 | # Kivy version to use 68 | osx.kivy_version = 1.9.1 69 | 70 | # 71 | # Android specific 72 | # 73 | 74 | # (bool) Indicate if the application should be fullscreen or not 75 | fullscreen = 0 76 | 77 | # (string) Presplash background color (for android toolchain) 78 | # Supported formats are: #RRGGBB #AARRGGBB or one of the following names: 79 | # red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray, 80 | # darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy, 81 | # olive, purple, silver, teal. 82 | #android.presplash_color = #FFFFFF 83 | 84 | # (string) Presplash animation using Lottie format. 85 | # see https://lottiefiles.com/ for examples and https://airbnb.design/lottie/ 86 | # for general documentation. 87 | # Lottie files can be created using various tools, like Adobe After Effect or Synfig. 88 | #android.presplash_lottie = "path/to/lottie/file.json" 89 | 90 | # (list) Permissions 91 | #android.permissions = INTERNET 92 | 93 | # (list) features (adds uses-feature -tags to manifest) 94 | #android.features = android.hardware.usb.host 95 | 96 | # (int) Target Android API, should be as high as possible. 97 | android.api = 34 98 | 99 | # (int) Minimum API your APK will support. 100 | #android.minapi = 21 101 | 102 | # (int) Android SDK version to use 103 | #android.sdk = 20 104 | 105 | # (str) Android NDK version to use 106 | #android.ndk = 19b 107 | 108 | # (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi. 109 | #android.ndk_api = 21 110 | 111 | # (bool) Use --private data storage (True) or --dir public storage (False) 112 | #android.private_storage = True 113 | 114 | # (str) Android NDK directory (if empty, it will be automatically downloaded.) 115 | #android.ndk_path = 116 | 117 | # (str) Android SDK directory (if empty, it will be automatically downloaded.) 118 | #android.sdk_path = 119 | 120 | # (str) ANT directory (if empty, it will be automatically downloaded.) 121 | #android.ant_path = 122 | 123 | # (bool) If True, then skip trying to update the Android sdk 124 | # This can be useful to avoid excess Internet downloads or save time 125 | # when an update is due and you just want to test/build your package 126 | # android.skip_update = False 127 | 128 | # (bool) If True, then automatically accept SDK license 129 | # agreements. This is intended for automation only. If set to False, 130 | # the default, you will be shown the license when first running 131 | # buildozer. 132 | android.accept_sdk_license = True 133 | 134 | # (str) Android entry point, default is ok for Kivy-based app 135 | #android.entrypoint = org.renpy.android.PythonActivity 136 | 137 | # (str) Android app theme, default is ok for Kivy-based app 138 | # android.apptheme = "@android:style/Theme.NoTitleBar" 139 | 140 | # (list) Pattern to whitelist for the whole project 141 | #android.whitelist = 142 | 143 | # (str) Path to a custom whitelist file 144 | #android.whitelist_src = 145 | 146 | # (str) Path to a custom blacklist file 147 | #android.blacklist_src = 148 | 149 | # (list) List of Java .jar files to add to the libs so that pyjnius can access 150 | # their classes. Don't add jars that you do not need, since extra jars can slow 151 | # down the build process. Allows wildcards matching, for example: 152 | # OUYA-ODK/libs/*.jar 153 | #android.add_jars = foo.jar,bar.jar,path/to/more/*.jar 154 | 155 | # (list) List of Java files to add to the android project (can be java or a 156 | # directory containing the files) 157 | android.add_src =libs/ 158 | 159 | # (list) Android AAR archives to add 160 | #android.add_aars = 161 | 162 | # (list) Gradle dependencies to add 163 | android.gradle_dependencies =com.google.firebase:firebase-messaging,com.google.firebase:firebase-analytics,com.google.code.gson:gson:2.8.6 164 | 165 | # (list) add java compile options 166 | # this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option 167 | # see https://developer.android.com/studio/write/java8-support for further information 168 | # android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8" 169 | 170 | # (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies} 171 | # please enclose in double quotes 172 | # e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }" 173 | #android.add_gradle_repositories = 174 | 175 | # (list) packaging options to add 176 | # see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html 177 | # can be necessary to solve conflicts in gradle_dependencies 178 | # please enclose in double quotes 179 | # e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'" 180 | #android.add_packaging_options = 181 | 182 | # (list) Java classes to add as activities to the manifest. 183 | #android.add_activities = com.example.ExampleActivity 184 | 185 | # (str) OUYA Console category. Should be one of GAME or APP 186 | # If you leave this blank, OUYA support will not be enabled 187 | #android.ouya.category = GAME 188 | 189 | # (str) Filename of OUYA Console icon. It must be a 732x412 png image. 190 | #android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png 191 | 192 | # (str) XML file to include as an intent filters in tag 193 | #android.manifest.intent_filters = 194 | 195 | # (str) launchMode to set for the main activity 196 | #android.manifest.launch_mode = standard 197 | 198 | # (list) Android additional libraries to copy into libs/armeabi 199 | #android.add_libs_armeabi = libs/android/*.so 200 | #android.add_libs_armeabi_v7a = libs/android-v7/*.so 201 | #android.add_libs_arm64_v8a = libs/android-v8/*.so 202 | #android.add_libs_x86 = libs/android-x86/*.so 203 | #android.add_libs_mips = libs/android-mips/*.so 204 | 205 | # (bool) Indicate whether the screen should stay on 206 | # Don't forget to add the WAKE_LOCK permission if you set this to True 207 | #android.wakelock = False 208 | 209 | # (list) Android application meta-data to set (key=value format) 210 | #android.meta_data = 211 | 212 | # (list) Android library project to add (will be added in the 213 | # project.properties automatically.) 214 | #android.library_references = 215 | 216 | # (list) Android shared libraries which will be added to AndroidManifest.xml using tag 217 | #android.uses_library = 218 | 219 | # (str) Android logcat filters to use 220 | #android.logcat_filters = *:S python:D 221 | 222 | # (bool) Copy library instead of making a libpymodules.so 223 | #android.copy_libs = 1 224 | 225 | # (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 226 | android.archs = arm64-v8a 227 | 228 | # (int) overrides automatic versionCode computation (used in build.gradle) 229 | # this is not the same as app version and should only be edited if you know what you're doing 230 | # android.numeric_version = 1 231 | 232 | # (bool) enables Android auto backup feature (Android API >=23) 233 | android.allow_backup = True 234 | 235 | # (str) XML file for custom backup rules (see official auto backup documentation) 236 | # android.backup_rules = 237 | 238 | # (str) If you need to insert variables into your AndroidManifest.xml file, 239 | # you can do so with the manifestPlaceholders property. 240 | # This property takes a map of key-value pairs. (via a string) 241 | # Usage example : android.manifest_placeholders = [myCustomUrl:\"org.kivy.customurl\"] 242 | # android.manifest_placeholders = [:] 243 | 244 | # 245 | # Python for android (p4a) specific 246 | # 247 | 248 | # (str) python-for-android fork to use, defaults to upstream (kivy) 249 | #p4a.fork = kivy 250 | 251 | # (str) python-for-android branch to use, defaults to master 252 | #p4a.branch = develop 253 | 254 | # (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) 255 | p4a.source_dir =/Users/thomas/Documents/projects/push_mac/python-for-android 256 | 257 | # (str) The directory in which python-for-android should look for your own build recipes (if any) 258 | #p4a.local_recipes = 259 | 260 | # (str) Filename to the hook for p4a 261 | #p4a.hook = 262 | 263 | # (str) Bootstrap to use for android builds 264 | # p4a.bootstrap = sdl2 265 | 266 | # (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) 267 | #p4a.port = 268 | 269 | # Control passing the --use-setup-py vs --ignore-setup-py to p4a 270 | # "in the future" --use-setup-py is going to be the default behaviour in p4a, right now it is not 271 | # Setting this to false will pass --ignore-setup-py, true will pass --use-setup-py 272 | # NOTE: this is general setuptools integration, having pyproject.toml is enough, no need to generate 273 | # setup.py if you're using Poetry, but you need to add "toml" to source.include_exts. 274 | #p4a.setup_py = false 275 | 276 | 277 | # 278 | # iOS specific 279 | # 280 | 281 | # (str) Path to a custom kivy-ios folder 282 | #ios.kivy_ios_dir = ../kivy-ios 283 | # Alternately, specify the URL and branch of a git checkout: 284 | ios.kivy_ios_url = https://github.com/kivy/kivy-ios 285 | ios.kivy_ios_branch = master 286 | 287 | # Another platform dependency: ios-deploy 288 | # Uncomment to use a custom checkout 289 | #ios.ios_deploy_dir = ../ios_deploy 290 | # Or specify URL and branch 291 | ios.ios_deploy_url = https://github.com/phonegap/ios-deploy 292 | ios.ios_deploy_branch = 1.10.0 293 | 294 | # (bool) Whether or not to sign the code 295 | ios.codesign.allowed = false 296 | 297 | # (str) Name of the certificate to use for signing the debug version 298 | # Get a list of available identities: buildozer ios list_identities 299 | #ios.codesign.debug = "iPhone Developer: ()" 300 | 301 | # (str) Name of the certificate to use for signing the release version 302 | #ios.codesign.release = %(ios.codesign.debug)s 303 | 304 | 305 | [buildozer] 306 | 307 | # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) 308 | log_level = 2 309 | 310 | # (int) Display warning if buildozer is run as root (0 = False, 1 = True) 311 | warn_on_root = 1 312 | 313 | # (str) Path to build artifact storage, absolute or relative to spec file 314 | # build_dir = ./.buildozer 315 | 316 | # (str) Path to build output (i.e. .apk, .ipa) storage 317 | # bin_dir = ./bin 318 | 319 | # ----------------------------------------------------------------------------- 320 | # List as sections 321 | # 322 | # You can define all the "list" as [section:key]. 323 | # Each line will be considered as a option to the list. 324 | # Let's take [app] / source.exclude_patterns. 325 | # Instead of doing: 326 | # 327 | #[app] 328 | #source.exclude_patterns = license,data/audio/*.wav,data/images/original/* 329 | # 330 | # This can be translated into: 331 | # 332 | #[app:source.exclude_patterns] 333 | #license 334 | #data/audio/*.wav 335 | #data/images/original/* 336 | # 337 | 338 | 339 | # ----------------------------------------------------------------------------- 340 | # Profiles 341 | # 342 | # You can extend section / key with a profile 343 | # For example, you want to deploy a demo version of your application without 344 | # HD content. You could first change the title to add "(demo)" in the name 345 | # and extend the excluded directories to remove the HD content. 346 | # 347 | #[app@demo] 348 | #title = My Application (demo) 349 | # 350 | #[app:source.exclude_patterns@demo] 351 | #images/hd/* 352 | # 353 | # Then, invoke the command line with the "demo" profile: 354 | # 355 | #buildozer --profile demo android debug 356 | -------------------------------------------------------------------------------- /src/python/libs/ContextHolder.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Chromium Authors. All rights reserved. 2 | package org.kivy.plugins.messaging; 3 | 4 | import android.content.Context; 5 | import android.util.Log; 6 | 7 | public class ContextHolder { 8 | private static Context applicationContext; 9 | 10 | public static Context getApplicationContext() { 11 | return applicationContext; 12 | } 13 | 14 | public static void setApplicationContext(Context applicationContext) { 15 | Log.d("KVFireContextHolder", "received application context."); 16 | ContextHolder.applicationContext = applicationContext; 17 | } 18 | } -------------------------------------------------------------------------------- /src/python/libs/KivyFirebaseMessagingBackgroundExecutor.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Chromium Authors. All rights reserved. 2 | package org.kivy.plugins.messaging; 3 | 4 | import android.util.Log; 5 | 6 | import java.util.concurrent.atomic.AtomicBoolean; 7 | 8 | public class KivyFirebaseMessagingBackgroundExecutor { 9 | private static AtomicBoolean started = new AtomicBoolean(false); 10 | public static void startBackgroundPythonService() { 11 | 12 | Log.d("BackgroundExecutor", "Starting background service"); 13 | com.waterfall.youtube.ServicePythonnotificationhandler.start(ContextHolder.getApplicationContext(), ""); 14 | Log.d("BackgroundExecutor", "Background service started"); 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/python/libs/KivyFirebaseMessagingBackgroundService.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Chromium Authors. All rights reserved. 2 | package org.kivy.plugins.messaging; 3 | 4 | 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.os.Handler; 8 | import android.util.Log; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.core.app.JobIntentService; 12 | 13 | import com.google.firebase.messaging.RemoteMessage; 14 | 15 | import java.util.Map; 16 | import java.util.concurrent.CountDownLatch; 17 | 18 | public class KivyFirebaseMessagingBackgroundService extends JobIntentService { 19 | private static final String TAG = "KVFireMsgService"; 20 | 21 | /** 22 | * Schedule the message to be handled by the {@link KivyFirebaseMessagingBackgroundService}. 23 | */ 24 | public static void enqueueMessageProcessing(Context context, Intent messageIntent) { 25 | enqueueWork( 26 | context, 27 | KivyFirebaseMessagingBackgroundService.class, 28 | KivyFirebaseMessagingUtils.JOB_ID, 29 | messageIntent); 30 | } 31 | 32 | @Override 33 | public void onCreate() { 34 | super.onCreate(); 35 | KivyFirebaseMessagingBackgroundExecutor.startBackgroundPythonService(); 36 | } 37 | 38 | @Override 39 | protected void onHandleWork(@NonNull final Intent intent) { 40 | 41 | 42 | // There were no pre-existing callback requests. Execute the callback 43 | // specified by the incoming intent. 44 | final CountDownLatch latch = new CountDownLatch(1); 45 | 46 | new Handler(getMainLooper()) 47 | .post(new Runnable() { 48 | @Override 49 | public void run() { 50 | RemoteMessage remoteMessage = 51 | intent.getParcelableExtra(KivyFirebaseMessagingUtils.EXTRA_REMOTE_MESSAGE); 52 | if (remoteMessage != null) { 53 | Map remoteMessageMap = 54 | KivyFirebaseMessagingUtils.remoteMessageToMap(remoteMessage); 55 | remoteMessageMap.put("unique_key", Math.random() + ""); 56 | PlatformIntermediate.addbackroundMessage(remoteMessageMap, ContextHolder.getApplicationContext()); 57 | } 58 | // End 59 | latch.countDown(); 60 | } 61 | } 62 | ); 63 | 64 | try { 65 | latch.await(); 66 | } catch (InterruptedException ex) { 67 | Log.i(TAG, "Exception waiting to execute Python callback", ex); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/python/libs/KivyFirebaseMessagingReceiver.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Chromium Authors. All rights reserved. 2 | package org.kivy.plugins.messaging; 3 | 4 | 5 | import android.content.BroadcastReceiver; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.util.Log; 9 | import com.google.firebase.messaging.RemoteMessage; 10 | import java.util.HashMap; 11 | 12 | 13 | public class KivyFirebaseMessagingReceiver extends BroadcastReceiver { 14 | private static final String TAG = "FLTFireMsgReceiver"; 15 | static HashMap notifications = new HashMap<>(); 16 | 17 | @Override 18 | public void onReceive(Context context, Intent intent) { 19 | Log.d(TAG, "broadcast received for message"); 20 | if (ContextHolder.getApplicationContext() == null) { 21 | ContextHolder.setApplicationContext(context.getApplicationContext()); 22 | } 23 | 24 | RemoteMessage remoteMessage = new RemoteMessage(intent.getExtras()); 25 | 26 | // Store the RemoteMessage if the message contains a notification payload. 27 | if (remoteMessage.getNotification() != null) { 28 | notifications.put(remoteMessage.getMessageId(), remoteMessage); 29 | KivyFirebaseMessagingStore.getInstance().storeFirebaseMessage(remoteMessage); 30 | } 31 | 32 | // |-> --------------------- 33 | // App in Foreground 34 | // ------------------------ 35 | if (KivyFirebaseMessagingUtils.isApplicationForeground(context)) { 36 | Log.d(TAG, "Setting the foreground."); 37 | if(remoteMessage.getNotification() != null){ 38 | Log.d("BTW, title is " + remoteMessage.getNotification().getTitle()); 39 | } 40 | PlatformIntermediate.setForegroundMessage(KivyFirebaseMessagingUtils.remoteMessageToMap(remoteMessage)); 41 | return; 42 | } 43 | 44 | // |-> --------------------- 45 | // App in Background/Quit 46 | // ------------------------ 47 | Log.d(TAG, "App in Background/Quit"); 48 | // HashMap payload = new HashMap<>(); 49 | // payload.put("unique_key", Math.random()); 50 | // payload.put("payload_type", "BACKGROUND_MSG"); 51 | // payload.put("data", KivyFirebaseMessagingUtils.remoteMessageToMap(remoteMessage)); 52 | // Gson gson = new Gson(); 53 | // String json = gson.toJson(payload); 54 | // backgroundMessages.put(Math.random()+"",json); 55 | // com.waterfall.youtube.ServicePythonnotificationhandler.start(org.kivy.android.PythonActivity.mActivity, json); 56 | // Issue with above is it relies on the Python service to be not running. Moment it's already running and you try starting 57 | // it, Android won't allow that. 58 | 59 | Intent onBackgroundMessageIntent = 60 | new Intent(context, org.kivy.plugins.messaging.KivyFirebaseMessagingBackgroundService.class); 61 | onBackgroundMessageIntent.putExtra( 62 | KivyFirebaseMessagingUtils.EXTRA_REMOTE_MESSAGE, remoteMessage); 63 | org.kivy.plugins.messaging.KivyFirebaseMessagingBackgroundService.enqueueMessageProcessing( 64 | context, onBackgroundMessageIntent); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/python/libs/KivyFirebaseMessagingService.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Chromium Authors. All rights reserved. 2 | package org.kivy.plugins.messaging; 3 | 4 | 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import com.google.firebase.messaging.FirebaseMessagingService; 9 | import com.google.firebase.messaging.RemoteMessage; 10 | 11 | public class KivyFirebaseMessagingService extends FirebaseMessagingService { 12 | @Override 13 | public void onNewToken(@NonNull String token) { 14 | PlatformIntermediate.token = token; 15 | } 16 | 17 | @Override 18 | public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { 19 | // Added for commenting purposes; 20 | // We don't handle the message here as we already handle it in the receiver and don't want to duplicate. 21 | } 22 | } -------------------------------------------------------------------------------- /src/python/libs/KivyFirebaseMessagingStore.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Chromium Authors. All rights reserved. 2 | package org.kivy.plugins.messaging; 3 | 4 | 5 | import android.content.Context; 6 | import android.content.SharedPreferences; 7 | import com.google.firebase.messaging.RemoteMessage; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.HashMap; 11 | import java.util.Iterator; 12 | import java.util.List; 13 | import java.util.Map; 14 | import org.json.JSONArray; 15 | import org.json.JSONException; 16 | import org.json.JSONObject; 17 | 18 | public class KivyFirebaseMessagingStore { 19 | private static final String PREFERENCES_FILE = "io.flutter.plugins.firebase.messaging"; 20 | private static final String KEY_NOTIFICATION_IDS = "notification_ids"; 21 | private static final int MAX_SIZE_NOTIFICATIONS = 20; 22 | private static KivyFirebaseMessagingStore instance; 23 | private final String DELIMITER = ","; 24 | private SharedPreferences preferences; 25 | 26 | public static KivyFirebaseMessagingStore getInstance() { 27 | if (instance == null) { 28 | instance = new KivyFirebaseMessagingStore(); 29 | } 30 | return instance; 31 | } 32 | 33 | private SharedPreferences getPreferences() { 34 | if (preferences == null) { 35 | preferences = 36 | ContextHolder.getApplicationContext() 37 | .getSharedPreferences(PREFERENCES_FILE, Context.MODE_PRIVATE); 38 | } 39 | return preferences; 40 | } 41 | 42 | public void setPreferencesStringValue(String key, String value) { 43 | getPreferences().edit().putString(key, value).apply(); 44 | } 45 | 46 | public String getPreferencesStringValue(String key, String defaultValue) { 47 | return getPreferences().getString(key, defaultValue); 48 | } 49 | 50 | public void storeFirebaseMessage(RemoteMessage remoteMessage) { 51 | String remoteMessageString = 52 | new JSONObject(KivyFirebaseMessagingUtils.remoteMessageToMap(remoteMessage)).toString(); 53 | setPreferencesStringValue(remoteMessage.getMessageId(), remoteMessageString); 54 | 55 | // Save new notification id. 56 | // Note that this is using a comma delimited string to preserve ordering. We could use a String Set 57 | // on SharedPreferences but this won't guarantee ordering when we want to remove the oldest added ids. 58 | String notifications = getPreferencesStringValue(KEY_NOTIFICATION_IDS, ""); 59 | notifications += remoteMessage.getMessageId() + DELIMITER; // append to last 60 | 61 | // Check and remove old notification messages. 62 | List allNotificationList = 63 | new ArrayList<>(Arrays.asList(notifications.split(DELIMITER))); 64 | if (allNotificationList.size() > MAX_SIZE_NOTIFICATIONS) { 65 | String firstRemoteMessageId = allNotificationList.get(0); 66 | getPreferences().edit().remove(firstRemoteMessageId).apply(); 67 | notifications = notifications.replace(firstRemoteMessageId + DELIMITER, ""); 68 | } 69 | 70 | setPreferencesStringValue(KEY_NOTIFICATION_IDS, notifications); 71 | } 72 | 73 | public RemoteMessage getFirebaseMessage(String remoteMessageId) { 74 | String remoteMessageString = getPreferencesStringValue(remoteMessageId, null); 75 | if (remoteMessageString != null) { 76 | try { 77 | Map argumentsMap = new HashMap<>(1); 78 | Map messageOutMap = jsonObjectToMap(new JSONObject(remoteMessageString)); 79 | // Add a fake 'to' - as it's required to construct a RemoteMessage instance. 80 | messageOutMap.put("to", remoteMessageId); 81 | argumentsMap.put("message", messageOutMap); 82 | return KivyFirebaseMessagingUtils.getRemoteMessageForArguments(argumentsMap); 83 | } catch (JSONException e) { 84 | e.printStackTrace(); 85 | } 86 | } 87 | return null; 88 | } 89 | 90 | public void removeFirebaseMessage(String remoteMessageId) { 91 | getPreferences().edit().remove(remoteMessageId).apply(); 92 | String notifications = getPreferencesStringValue(KEY_NOTIFICATION_IDS, ""); 93 | if (!notifications.isEmpty()) { 94 | notifications = notifications.replace(remoteMessageId + DELIMITER, ""); 95 | setPreferencesStringValue(KEY_NOTIFICATION_IDS, notifications); 96 | } 97 | } 98 | 99 | private Map jsonObjectToMap(JSONObject jsonObject) throws JSONException { 100 | Map map = new HashMap<>(); 101 | Iterator keys = jsonObject.keys(); 102 | while (keys.hasNext()) { 103 | String key = keys.next(); 104 | Object value = jsonObject.get(key); 105 | if (value instanceof JSONArray) { 106 | value = jsonArrayToList((JSONArray) value); 107 | } else if (value instanceof JSONObject) { 108 | value = jsonObjectToMap((JSONObject) value); 109 | } 110 | map.put(key, value); 111 | } 112 | return map; 113 | } 114 | 115 | public List jsonArrayToList(JSONArray array) throws JSONException { 116 | List list = new ArrayList<>(); 117 | for (int i = 0; i < array.length(); i++) { 118 | Object value = array.get(i); 119 | if (value instanceof JSONArray) { 120 | value = jsonArrayToList((JSONArray) value); 121 | } else if (value instanceof JSONObject) { 122 | value = jsonObjectToMap((JSONObject) value); 123 | } 124 | list.add(value); 125 | } 126 | return list; 127 | } 128 | } -------------------------------------------------------------------------------- /src/python/libs/KivyFirebaseMessagingUtils.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Chromium Authors. All rights reserved. 2 | package org.kivy.plugins.messaging; 3 | 4 | import android.app.ActivityManager; 5 | import android.app.KeyguardManager; 6 | import android.content.Context; 7 | import com.google.firebase.messaging.FirebaseMessaging; 8 | import com.google.firebase.messaging.RemoteMessage; 9 | import java.util.Arrays; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Objects; 14 | import java.util.Set; 15 | 16 | class KivyFirebaseMessagingUtils { 17 | static final String IS_AUTO_INIT_ENABLED = "isAutoInitEnabled"; 18 | static final String SHARED_PREFERENCES_KEY = "io.flutter.firebase.messaging.callback"; 19 | static final String ACTION_REMOTE_MESSAGE = "io.flutter.plugins.firebase.messaging.NOTIFICATION"; 20 | static final String EXTRA_REMOTE_MESSAGE = "notification"; 21 | static final String ACTION_TOKEN = "io.flutter.plugins.firebase.messaging.TOKEN"; 22 | static final String EXTRA_TOKEN = "token"; 23 | static final int JOB_ID = 2020; 24 | private static final String KEY_COLLAPSE_KEY = "collapseKey"; 25 | private static final String KEY_DATA = "data"; 26 | private static final String KEY_FROM = "from"; 27 | private static final String KEY_MESSAGE_ID = "messageId"; 28 | private static final String KEY_MESSAGE_TYPE = "messageType"; 29 | private static final String KEY_SENT_TIME = "sentTime"; 30 | private static final String KEY_TO = "to"; 31 | private static final String KEY_TTL = "ttl"; 32 | 33 | static Map remoteMessageToMap(RemoteMessage remoteMessage) { 34 | Map messageMap = new HashMap<>(); 35 | Map dataMap = new HashMap<>(); 36 | 37 | if (remoteMessage.getCollapseKey() != null) { 38 | messageMap.put(KEY_COLLAPSE_KEY, remoteMessage.getCollapseKey()); 39 | } 40 | 41 | if (remoteMessage.getFrom() != null) { 42 | messageMap.put(KEY_FROM, remoteMessage.getFrom()); 43 | } 44 | 45 | if (remoteMessage.getTo() != null) { 46 | messageMap.put(KEY_TO, remoteMessage.getTo()); 47 | } 48 | 49 | if (remoteMessage.getMessageId() != null) { 50 | messageMap.put(KEY_MESSAGE_ID, remoteMessage.getMessageId()); 51 | } 52 | 53 | if (remoteMessage.getMessageType() != null) { 54 | messageMap.put(KEY_MESSAGE_TYPE, remoteMessage.getMessageType()); 55 | } 56 | 57 | if (remoteMessage.getData().size() > 0) { 58 | Set> entries = remoteMessage.getData().entrySet(); 59 | for (Map.Entry entry : entries) { 60 | dataMap.put(entry.getKey(), entry.getValue()); 61 | } 62 | } 63 | 64 | messageMap.put(KEY_DATA, dataMap); 65 | messageMap.put(KEY_TTL, remoteMessage.getTtl()); 66 | messageMap.put(KEY_SENT_TIME, remoteMessage.getSentTime()); 67 | 68 | if (remoteMessage.getNotification() != null) { 69 | messageMap.put( 70 | "notification", remoteMessageNotificationToMap(remoteMessage.getNotification())); 71 | } 72 | 73 | return messageMap; 74 | } 75 | 76 | private static Map remoteMessageNotificationToMap( 77 | RemoteMessage.Notification notification) { 78 | Map notificationMap = new HashMap<>(); 79 | Map androidNotificationMap = new HashMap<>(); 80 | 81 | if (notification.getTitle() != null) { 82 | notificationMap.put("title", notification.getTitle()); 83 | } 84 | 85 | if (notification.getTitleLocalizationKey() != null) { 86 | notificationMap.put("titleLocKey", notification.getTitleLocalizationKey()); 87 | } 88 | 89 | if (notification.getTitleLocalizationArgs() != null) { 90 | notificationMap.put("titleLocArgs", Arrays.asList(notification.getTitleLocalizationArgs())); 91 | } 92 | 93 | if (notification.getBody() != null) { 94 | notificationMap.put("body", notification.getBody()); 95 | } 96 | 97 | if (notification.getBodyLocalizationKey() != null) { 98 | notificationMap.put("bodyLocKey", notification.getBodyLocalizationKey()); 99 | } 100 | 101 | if (notification.getBodyLocalizationArgs() != null) { 102 | notificationMap.put("bodyLocArgs", Arrays.asList(notification.getBodyLocalizationArgs())); 103 | } 104 | 105 | if (notification.getChannelId() != null) { 106 | androidNotificationMap.put("channelId", notification.getChannelId()); 107 | } 108 | 109 | if (notification.getClickAction() != null) { 110 | androidNotificationMap.put("clickAction", notification.getClickAction()); 111 | } 112 | 113 | if (notification.getColor() != null) { 114 | androidNotificationMap.put("color", notification.getColor()); 115 | } 116 | 117 | if (notification.getIcon() != null) { 118 | androidNotificationMap.put("smallIcon", notification.getIcon()); 119 | } 120 | 121 | if (notification.getImageUrl() != null) { 122 | androidNotificationMap.put("imageUrl", notification.getImageUrl().toString()); 123 | } 124 | 125 | if (notification.getLink() != null) { 126 | androidNotificationMap.put("link", notification.getLink().toString()); 127 | } 128 | 129 | if (notification.getNotificationCount() != null) { 130 | androidNotificationMap.put("count", notification.getNotificationCount()); 131 | } 132 | 133 | if (notification.getNotificationPriority() != null) { 134 | androidNotificationMap.put("priority", notification.getNotificationPriority()); 135 | } 136 | 137 | if (notification.getSound() != null) { 138 | androidNotificationMap.put("sound", notification.getSound()); 139 | } 140 | 141 | if (notification.getTicker() != null) { 142 | androidNotificationMap.put("ticker", notification.getTicker()); 143 | } 144 | 145 | if (notification.getVisibility() != null) { 146 | androidNotificationMap.put("visibility", notification.getVisibility()); 147 | } 148 | 149 | if (notification.getTag() != null) { 150 | androidNotificationMap.put("tag", notification.getTag()); 151 | } 152 | 153 | notificationMap.put("android", androidNotificationMap); 154 | return notificationMap; 155 | } 156 | 157 | /** 158 | * Identify if the application is currently in a state where user interaction is possible. This 159 | * method is called when a remote message is received to determine how the incoming message should 160 | * be handled. 161 | * 162 | * @param context context. 163 | * @return True if the application is currently in a state where user interaction is possible, 164 | * false otherwise. 165 | */ 166 | static boolean isApplicationForeground(Context context) { 167 | KeyguardManager keyguardManager = 168 | (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); 169 | 170 | if (keyguardManager != null && keyguardManager.isKeyguardLocked()) { 171 | return false; 172 | } 173 | 174 | ActivityManager activityManager = 175 | (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 176 | if (activityManager == null) return false; 177 | 178 | List appProcesses = 179 | activityManager.getRunningAppProcesses(); 180 | if (appProcesses == null) return false; 181 | 182 | final String packageName = context.getPackageName(); 183 | for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { 184 | if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND 185 | && appProcess.processName.equals(packageName)) { 186 | return true; 187 | } 188 | } 189 | 190 | return false; 191 | } 192 | 193 | // Extracted to handle multi-app support in the future. 194 | // arguments.get("appName") - to get the Firebase app name. 195 | static FirebaseMessaging getFirebaseMessagingForArguments(Map arguments) { 196 | return FirebaseMessaging.getInstance(); 197 | } 198 | 199 | /** 200 | * Builds an instance of {@link RemoteMessage} from Flutter method channel call arguments. 201 | * 202 | * @param arguments Method channel call arguments. 203 | * @return RemoteMessage 204 | */ 205 | static RemoteMessage getRemoteMessageForArguments(Map arguments) { 206 | @SuppressWarnings("unchecked") 207 | Map messageMap = 208 | (Map) Objects.requireNonNull(arguments.get("message")); 209 | 210 | String to = (String) Objects.requireNonNull(messageMap.get("to")); 211 | RemoteMessage.Builder builder = new RemoteMessage.Builder(to); 212 | 213 | String collapseKey = (String) messageMap.get("collapseKey"); 214 | String messageId = (String) messageMap.get("messageId"); 215 | String messageType = (String) messageMap.get("messageType"); 216 | Integer ttl = (Integer) messageMap.get("ttl"); 217 | 218 | @SuppressWarnings("unchecked") 219 | Map data = (Map) messageMap.get("data"); 220 | 221 | if (collapseKey != null) { 222 | builder.setCollapseKey(collapseKey); 223 | } 224 | 225 | if (messageType != null) { 226 | builder.setMessageType(messageType); 227 | } 228 | 229 | if (messageId != null) { 230 | builder.setMessageId(messageId); 231 | } 232 | 233 | if (ttl != null) { 234 | builder.setTtl(ttl); 235 | } 236 | 237 | if (data != null) { 238 | builder.setData(data); 239 | } 240 | 241 | return builder.build(); 242 | } 243 | } -------------------------------------------------------------------------------- /src/python/libs/PlatformIntermediate.java: -------------------------------------------------------------------------------- 1 | package org.kivy.plugins.messaging; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.google.gson.Gson; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.BufferedWriter; 10 | import java.io.FileNotFoundException; 11 | import java.io.FileOutputStream; 12 | import java.io.FileReader; 13 | import java.io.IOException; 14 | import java.io.OutputStreamWriter; 15 | import java.io.UnsupportedEncodingException; 16 | import java.io.Writer; 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | 20 | public class PlatformIntermediate { 21 | private static HashMap _foregroundMessage = new HashMap(); 22 | final public static HashMap backgroundMessages = new HashMap(); 23 | public static String token = ""; 24 | private static final String BACKGROUND_MSG_FILE_NAME = "/background_messages.json"; 25 | 26 | public static String getForegroundMessage() { 27 | return new Gson().toJson(_foregroundMessage); 28 | } 29 | 30 | public static void setForegroundMessage(Map msg) { 31 | HashMap copy = new HashMap(msg); 32 | _foregroundMessage = copy; 33 | 34 | _foregroundMessage.put("unique_key", Math.random()); 35 | Log.d("PlatformIntermediate", "setForegroundMessage worked " + _foregroundMessage.toString()); 36 | } 37 | 38 | public static void addbackroundMessage(Map remoteMessageMap, Context context) { 39 | try { 40 | com.waterfall.youtube.ServicePythonnotificationhandler.start(org.kivy.android.PythonActivity.mActivity, ""); 41 | } catch (Exception e) { 42 | Log.d("PlatformIntermediate", "Exception occurred while trying to start service: " + e.getMessage()); 43 | } 44 | backgroundMessages.put(remoteMessageMap.get("unique_key").toString(), remoteMessageMap); 45 | 46 | // Read stored json 47 | String jsonText = readBackgroundFile(context); 48 | 49 | Map map = new HashMap(); 50 | if (jsonText.length() > 0) { 51 | map = (Map) new Gson().fromJson(jsonText, map.getClass()); 52 | } 53 | boolean shouldRecreateFile = false; 54 | // Merge with map where key does not exist or create file if map size is 0 55 | if (map.size() == 0) { 56 | shouldRecreateFile = true; 57 | } else { 58 | for (String key : map.keySet()) { 59 | if (backgroundMessages.get(key) == null) { 60 | backgroundMessages.put(key, map.get(key)); 61 | shouldRecreateFile = true; 62 | } 63 | } 64 | } 65 | // Write to file 66 | if (shouldRecreateFile) { 67 | // deleteBackgroundFile(context); 68 | writeBackgroundFile(new Gson().toJson(backgroundMessages), context); 69 | 70 | } 71 | Log.d("PlatformIntermediate", "New message added, recreated: "+shouldRecreateFile +" "+context.getFilesDir().getPath()); 72 | if(shouldRecreateFile == false){ 73 | Log.d("PlatformIntermediate", map.toString()); 74 | 75 | } 76 | } 77 | 78 | public static void writeBackgroundFile(String data, Context context) { 79 | try (Writer writer = new BufferedWriter(new OutputStreamWriter( 80 | new FileOutputStream(context.getFilesDir().getPath()+BACKGROUND_MSG_FILE_NAME), "utf-8"))) { 81 | writer.write(data); 82 | } catch (UnsupportedEncodingException e) { 83 | e.printStackTrace(); 84 | } catch (FileNotFoundException e) { 85 | e.printStackTrace(); 86 | } catch (IOException e) { 87 | e.printStackTrace(); 88 | } 89 | } 90 | 91 | public static String readBackgroundFile(Context context) { 92 | String ret = ""; 93 | try (BufferedReader br = new BufferedReader(new FileReader(context.getFilesDir().getPath()+BACKGROUND_MSG_FILE_NAME))) { 94 | StringBuilder sb = new StringBuilder(); 95 | String line = br.readLine(); 96 | while (line != null) { 97 | sb.append(line); 98 | sb.append("\n"); 99 | line = br.readLine(); 100 | } 101 | ret = sb.toString(); 102 | } catch (IOException e) { 103 | e.printStackTrace(); 104 | } 105 | return ret; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/python/main.py: -------------------------------------------------------------------------------- 1 | from jnius import autoclass, java_method, PythonJavaClass 2 | 3 | from kivy.app import App 4 | from kivy.uix.button import Button 5 | 6 | from kivy.lang.builder import Builder 7 | from kivy.properties import DictProperty 8 | from kivy.properties import ObjectProperty 9 | from pushyy import Pushyy 10 | from pushyy import RemoteMessage 11 | 12 | KV = """ 13 | BoxLayout: 14 | orientation: "vertical" 15 | ScrollableLabel: 16 | text: str(app.recent_notification_data) 17 | font_size: 50 18 | text_size: self.width, None 19 | size_hint_y: None 20 | height: self.texture_size[1] 21 | Button: 22 | text: "get token" 23 | on_release: app.get_token() 24 | 25 | """ 26 | 27 | 28 | class TestApp(App): 29 | recent_notification_data = DictProperty(rebind=True) 30 | 31 | def on_start(self): 32 | Pushyy().foreground_message_handler(my_foreground_callback) 33 | Pushyy().notification_click_handler(my_notification_click_callback) 34 | Pushyy().token_change_listener(new_token_callback) 35 | 36 | def get_token(self): 37 | Pushyy().get_device_token(my_token_callback) 38 | 39 | def build(self): 40 | return Builder.load_string(KV) 41 | 42 | 43 | def my_token_callback(token): 44 | print(token) 45 | 46 | 47 | def my_foreground_callback(data: RemoteMessage): 48 | print(data) 49 | App.get_running_app().recent_notification_data = data.as_dict() 50 | 51 | 52 | def my_notification_click_callback(data: RemoteMessage): 53 | print(data) 54 | App.get_running_app().recent_notification_data = data.as_dict() 55 | 56 | 57 | def new_token_callback(data): 58 | print(data) 59 | 60 | 61 | TestApp().run() 62 | -------------------------------------------------------------------------------- /src/python/pushyy/__init__.py: -------------------------------------------------------------------------------- 1 | from .pushyy import * 2 | from .remote_message import RemoteMessage -------------------------------------------------------------------------------- /src/python/pushyy/pushyy.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from .remote_message import RemoteMessage 5 | from typing import Callable 6 | from typing import Any 7 | from kivy.clock import Clock, mainthread 8 | from jnius import autoclass, cast, java_method, PythonJavaClass 9 | 10 | 11 | class Pushyy: 12 | __notification_click_callback = None 13 | __last_on_message_key = None 14 | __token = None 15 | 16 | def foreground_message_handler( 17 | self, callback: Callable[[RemoteMessage], None], interval: float = 0.5 18 | ) -> None: 19 | """Function to call when push notification is received 20 | and application is in the foreground 21 | 22 | Example 23 | def my_foreground_callback(data: RemoteMessage): 24 | print(data) 25 | 26 | Parameters: 27 | callback (function): Callback function reference 28 | interval (float): How often to check for message 29 | 30 | Returns: 31 | None 32 | 33 | """ 34 | 35 | @mainthread 36 | def checker(*args): 37 | try: 38 | self.__on_message(callback) 39 | except Exception as e: 40 | print(e) 41 | 42 | Clock.schedule_interval(checker, interval) 43 | 44 | def __on_message(self, callback): 45 | PlatformIntermediate = autoclass( 46 | "org.kivy.plugins.messaging.PlatformIntermediate" 47 | ) 48 | data = PlatformIntermediate.getForegroundMessage() 49 | msg = json.loads(data) 50 | # Avoid "processing" an already "processed" message 51 | if len(msg) == 0 or msg.get("unique_key") == self.__last_on_message_key: 52 | pass 53 | else: 54 | self.__last_on_message_key = msg.get("unique_key") 55 | msg.pop("unique_key") 56 | callback(RemoteMessage(msg)) 57 | 58 | def notification_click_handler(self, callback: Callable[[RemoteMessage], None]): 59 | """Function to call when push notification is clicked 60 | 61 | Example 62 | def my_click_callback(data: RemoteMessage): 63 | print(data) 64 | 65 | Parameters: 66 | callback (function): Callback function reference 67 | 68 | Returns: 69 | None 70 | 71 | """ 72 | self.__notification_click_callback = callback 73 | 74 | # gg https://github.com/spesmilo/electrum/blob/6650e6bbae12a79e12667857ee039f1b1f30c7e3/electrum/gui/kivy/main_window.py#L620 75 | from android import activity 76 | 77 | PythonActivity = autoclass("org.kivy.android.PythonActivity") 78 | mactivity = PythonActivity.mActivity 79 | # Bind to when application appears on the foreground 80 | self.__on_new_intent(mactivity.getIntent()) 81 | activity.bind(on_new_intent=self.__on_new_intent) 82 | 83 | def __on_new_intent(self, intent): 84 | bundle = intent.getExtras() 85 | # Make sure it's a push notification 86 | if bundle is not None and ( 87 | bundle.get("google.message_id") != None or bundle.get("message_id") 88 | ): 89 | # Go through Java HashMap copying over to Python dictionary 90 | notification_data = {} 91 | for key in bundle.keySet(): 92 | notification_data[key] = bundle.get(key) 93 | self.__notification_click_callback(RemoteMessage(notification_data)) 94 | 95 | def get_device_token(self, callback: Callable[[str], None]) -> None: 96 | """Function to call when device token is retrieved 97 | 98 | Example 99 | def my_token_callback(token: str): 100 | print(token) 101 | 102 | Parameters: 103 | callback (function): Callback function reference 104 | 105 | Returns: 106 | None 107 | 108 | """ 109 | 110 | class MyTokenListener(PythonJavaClass): 111 | __javainterfaces__ = ["com/google/android/gms/tasks/OnSuccessListener"] 112 | __javacontext__ = "app" 113 | 114 | @java_method("(Ljava/lang/Object;)V") 115 | def onSuccess(self, s): 116 | callback(s) 117 | 118 | FirebaseMessaging = autoclass("com.google.firebase.messaging.FirebaseMessaging") 119 | FirebaseMessaging.getInstance().getToken().addOnSuccessListener( 120 | MyTokenListener() 121 | ) 122 | 123 | def token_change_listener( 124 | self, callback: Callable[[dict], None], interval: float = 0.5 125 | ) -> None: 126 | """Function to call when device token changes 127 | 128 | Example 129 | def new_token_callback(data: str): 130 | print(data) 131 | 132 | Parameters: 133 | callback (function): Callback function reference 134 | interval (float): How often to check for message 135 | 136 | Returns: 137 | None 138 | 139 | """ 140 | 141 | @mainthread 142 | def checker(*args): 143 | try: 144 | self.__on_new_token(callback) 145 | except Exception as e: 146 | print(e) 147 | 148 | Clock.schedule_interval(checker, interval) 149 | 150 | def __on_new_token(self, callback): 151 | PlatformIntermediate = autoclass( 152 | "org.kivy.plugins.messaging.PlatformIntermediate" 153 | ) 154 | token = PlatformIntermediate.token 155 | # Overwriting an already read "token" 156 | if len(token) == 0 or token == self.__token: 157 | pass 158 | else: 159 | self.__token = token 160 | callback(token) 161 | 162 | 163 | last_read_background_keys = None 164 | 165 | 166 | def process_background_messages(callback: Callable[[dict], None]): 167 | global last_read_background_keys 168 | """Function to call when push notification is received and app is not running 169 | Possible use-case is marking message as delivered in a chat application 170 | 171 | Example 172 | def my_background_callback(data: dict): 173 | print(data) 174 | 175 | Parameters: 176 | callback (function): Callback function reference 177 | 178 | Returns: 179 | None 180 | 181 | """ 182 | from jnius import autoclass 183 | import time 184 | 185 | try: 186 | PythonService = autoclass("org.kivy.android.PythonService") 187 | except Exception as e: 188 | # 'Request to get environment variables before JNI is ready' 189 | print(e) 190 | # Not sure if this is necessary, anyway 191 | import jnius 192 | reload(jnius) 193 | from jnius import autoclass 194 | import time 195 | time.sleep(3) 196 | PythonService = autoclass("org.kivy.android.PythonService") 197 | 198 | context = PythonService.mService.getApplicationContext() 199 | file_path = context.getFilesDir().getPath() + "/background_messages.json" 200 | if os.path.exists(file_path): 201 | """ 202 | Reason to read from a file and not the backgroundMessages variable is that from my 203 | observation, over JNI, the backgroundMessages variable gets reset to empty. Almost as 204 | if a new version of a static class is created ¯\_(ツ)_/¯ 205 | """ 206 | use_callback = False 207 | with open(file_path) as f: 208 | content = json.loads(f.read()) 209 | if last_read_background_keys != content.keys(): 210 | last_read_background_keys = content.keys() 211 | use_callback = True 212 | 213 | # Delete file since notification data is read 214 | os.remove(file_path) 215 | # Ensure old data is not passed to callback 216 | for key, data in content.items(): 217 | if use_callback: 218 | data.pop("unique_key") 219 | callback(RemoteMessage(data)) 220 | -------------------------------------------------------------------------------- /src/python/pushyy/remote_message.py: -------------------------------------------------------------------------------- 1 | class Notification: 2 | title: str 3 | body: str 4 | 5 | def __init__(self, notification_dict): 6 | self.title = notification_dict.get("title") 7 | self.body = notification_dict.get("body") 8 | 9 | def as_dict(self): 10 | return { 11 | "title": self.title, 12 | "body": self.body 13 | } 14 | 15 | class RemoteMessage: 16 | notification: Notification = None # None when message originates from notification click 17 | data: dict = {} 18 | message_id: str = "" 19 | sent_time: int = 0 20 | from_: str = "" 21 | ttl: int = 0 22 | 23 | def __init__(self, push_notification: dict): 24 | # Check if it's a notification click 25 | if push_notification.get("notification") is None: 26 | self.sent_time = push_notification.pop("google.sent_time") 27 | self.from_ = push_notification.pop("from") 28 | self.message_id = push_notification.pop("google.message_id") 29 | self.ttl = push_notification.pop("google.ttl") 30 | # Remove the unused keys, leaving only user data 31 | for e in ["collapse_key", "google.original_priority", "google.delivered_priority", "gcm.n.analytics_data"]: 32 | push_notification.pop(e) 33 | self.data = push_notification 34 | else: 35 | self.notification = Notification(push_notification["notification"]) 36 | self.data = push_notification.get("data") 37 | self.message_id = push_notification.get("messageId") 38 | self.from_ = push_notification.get("from") 39 | self.ttl = push_notification.get("ttl") 40 | self.sent_time = push_notification.get("sentTime") 41 | 42 | def as_dict(self): 43 | notif = {} 44 | if self.notification is not None: 45 | notif = self.notification.as_dict() 46 | return { 47 | "notification": notif, 48 | "data": self.data, 49 | "message_id": self.message_id, 50 | "sent_time": self.sent_time, 51 | "from": self.from_, 52 | "ttl": self.ttl 53 | } 54 | 55 | def __repr__(self): 56 | return str(self.as_dict()) 57 | -------------------------------------------------------------------------------- /src/python/python_notification_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | from pushyy import RemoteMessage, process_background_messages 4 | 5 | def my_background_callback(data: RemoteMessage) -> None: 6 | # ..your code goes here.. 7 | """ 8 | One of the things you can do here: Mark a chat message 9 | as delivered by making a request to your server 10 | """ 11 | print(data) 12 | try: 13 | requests.post("http://192.168.0.171:5000/ac", json = data) 14 | except: 15 | pass 16 | 17 | if __name__ == '__main__': 18 | for _ in range(3): 19 | try: 20 | process_background_messages(my_background_callback) 21 | except Exception as e: 22 | # Meh, run the loop again xD 23 | print(e) 24 | time.sleep(0.1) 25 | --------------------------------------------------------------------------------