├── .editorconfig
├── .gitmodules
├── .woodpecker
└── main.yml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── fcm
│ ├── AndroidManifest.xml
│ ├── google-services.json
│ └── java
│ │ └── org
│ │ └── unifiedpush
│ │ └── example
│ │ └── EmbeddedDistributor.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ └── org
│ │ └── unifiedpush
│ │ └── example
│ │ ├── ApplicationServer.kt
│ │ ├── PushServiceImpl.kt
│ │ ├── Store.kt
│ │ ├── TestService.kt
│ │ ├── Tests.kt
│ │ ├── Urgency.kt
│ │ ├── activities
│ │ ├── AppBarViewModel.kt
│ │ ├── CheckActivity.kt
│ │ ├── CheckViewModel.kt
│ │ ├── Events.kt
│ │ ├── MainActivity.kt
│ │ └── ui
│ │ │ ├── AppBarUi.kt
│ │ │ ├── AppBarUiState.kt
│ │ │ ├── CheckUi.kt
│ │ │ ├── CheckUiState.kt
│ │ │ ├── MainUi.kt
│ │ │ ├── PermissionsUi.kt
│ │ │ ├── SetUrgencyDialog.kt
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ └── utils
│ │ ├── DelayedRequestWorker.kt
│ │ ├── MessageParser.kt
│ │ ├── Notifier.kt
│ │ ├── RawRequest.kt
│ │ ├── RegistrationDialogs.kt
│ │ ├── Tag.kt
│ │ └── Utils.kt
│ └── res
│ ├── drawable
│ ├── ic_launcher_foreground.xml
│ └── ic_launcher_notification.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── values
│ ├── ic_launcher_background.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ └── network_security_config.xml
├── build.gradle
├── connector
├── connector_ui
├── distributor
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── changelogs
│ ├── 10.txt
│ ├── 11.txt
│ ├── 12.txt
│ ├── 13.txt
│ ├── 14.txt
│ ├── 15.txt
│ ├── 16.txt
│ ├── 17.txt
│ ├── 18.txt
│ ├── 19.txt
│ ├── 20.txt
│ ├── 21.txt
│ ├── 22.txt
│ ├── 23.txt
│ ├── 24.txt
│ ├── 25.txt
│ ├── 26.txt
│ └── 27.txt
│ ├── full_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── 1.png
│ │ ├── 2.png
│ │ └── 3.png
│ ├── short_description.txt
│ └── title.txt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{kt,kts}]
2 | ktlint_code_style = android_studio
3 | ktlint_function_naming_ignore_when_annotated_with=Composable
4 | max_line_length = 140
5 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/.gitmodules
--------------------------------------------------------------------------------
/.woodpecker/main.yml:
--------------------------------------------------------------------------------
1 | steps:
2 | check:
3 | # https://github.com/MobileDevOps/android-sdk-image
4 | image: mobiledevops/android-sdk-image:latest
5 | when:
6 | branch: main
7 | event: [push, pull_request, manual]
8 | commands:
9 | - ./gradlew build --stacktrace
10 | - mv app/build/outputs/apk/fcm/debug/app-fcm-debug.apk UP-Example-fcm.apk
11 | - mv app/build/outputs/apk/mainFlavor/debug/app-mainFlavor-debug.apk UP-Example-main.apk
12 |
13 | build:
14 | # https://github.com/MobileDevOps/android-sdk-image
15 | image: mobiledevops/android-sdk-image:latest
16 | when:
17 | branch: main
18 | event: tag
19 | commands:
20 | - export RELEASE_STORE_FILE=$PWD/release-key.jks
21 | - echo $RELEASE_KEY | base64 -d > $RELEASE_STORE_FILE
22 | - ./gradlew -Psign assembleRelease --stacktrace
23 | - mv app/build/outputs/apk/fcm/release/app-fcm-release.apk UP-Example-fcm.apk
24 | - mv app/build/outputs/apk/mainFlavor/release/app-mainFlavor-release.apk UP-Example-main.apk
25 | environment:
26 | RELEASE_KEY_ALIAS: unifiedpush
27 | RELEASE_KEY:
28 | from_secret: release_key
29 | RELEASE_STORE_PASSWORD:
30 | from_secret: release_store_password
31 | RELEASE_KEY_PASSWORD:
32 | from_secret: release_key_password
33 |
34 | upload:
35 | image: codeberg.org/s1m/woodpecker-upload:latest
36 | when:
37 | branch: main
38 | event: [push, pull_request, tag, manual]
39 | settings:
40 | token:
41 | from_secret: codeberg_token
42 | file:
43 | - UP-Example-main.apk
44 | - UP-Example-fcm.apk
45 | fastlane: true
46 | package: true
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2021 Simon Gougeon
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UnifiedPush Example
2 |
3 | This application is a generic application to handle notifications using UnifiedPush which can be used to test your setup. It is an example how to use [the UnifiedPush library](https://codeberg.org/UnifiedPush/android-connector).
4 |
5 | [
](https://f-droid.org/packages/org.unifiedpush.example/)
8 |
9 | ## Receive notifications from a terminal
10 |
11 | You can use this app as a rustic application to receive notifications send from a terminal/a process
12 |
13 | #### Via encrypted WebPush requests
14 |
15 | Toggle ON "WebPush" before registering on UP-Example, and send WebPush requests.
16 |
17 | With a python script for instance:
18 |
19 | ```py
20 | #!/usr/bin/env python
21 |
22 | from pywebpush import webpush
23 | import urllib
24 | import sys
25 |
26 | if len(sys.argv) < 2:
27 | print("Usage: {} message".format(sys.argv[0]))
28 |
29 | subinfo = {
30 | "endpoint": "YOUR_ENDPOINT_HERE",
31 | "keys": {
32 | "auth": "AUTH_SECRET_HERE",
33 | "p256dh": "P256DH_SECRET_HERE"
34 | }
35 | }
36 |
37 | message = "title=UP!&message=" + urllib.parse.quote(' '.join(sys.argv[1::]))
38 |
39 | webpush(subinfo, message)
40 | ```
41 |
42 | To use it: `./notify.py My message here`
43 |
44 | Depending on your distributor, you may need to set the VAPID header too: add `headers={"authorization": "vapid t=[...],k=[...]"}` to the webpush call.
45 |
46 | #### Via unencrypted requests
47 |
48 | Push notifications are intended to be encrypted. But you can send unencrypted requests by sending HTTP POST requests with the header `Content-Encoding: aes128gcm`.
49 | Depending on your distributor, you may need to set the VAPID header too: add `Authorization: vapid t=[...],k=[...]`.
50 |
51 | For instance with cURL:
52 |
53 | ```
54 | curl -X POST $endpoint -H "Content-encoding: aes128gcm" --data "title={Your Title}&message={Your Message}"
55 | ```
56 |
57 | ## Developer mode, to test a distributor
58 |
59 | This application can be used to test different features of a distributor. To enable this mode, check `Developer mode` in the upper right menu.
60 |
61 | You will be able to:
62 | - Show an error notification if the received message hasn't been correctly decrypted, by checking `Error if decryption fails`.
63 | - Use VAPID:
64 | - After toggling this setting or after renewing VAPID key, you must "reregister" your application. This is not done automatically to allow testing different cases.
65 | - The VAPID header is cached for 5 minutes: following RFC8292, push servers should cache the JWT to avoid checking the signature every time. This allows to test it.
66 | - Send cleartext messages
67 | - Use wrong VAPID keys, you should not receive new messages.
68 | - Use wrong encryption keys, decryption for new messages will fail.
69 | - Start a foreground service when a message is received, by checking `Foreground service on message`. It must work even if the example application has optimized battery, and is in the background.
70 | - Resend registration message, by clicking on `Reregister`.
71 | - Start the link activity using deep link by clicking on `Deep link`.
72 | - Change the distributor, without using the deep link by clicking on `Change distributor`.
73 | - Set urgency for new messages.
74 | - Test TTL. The TTL is correctly implemented if you don't receive the test message. You will have to disconnect the distributor during the process.
75 | - Test topics. The topics are correctly implemented if you don't receive the 1st test message which would have been replaced by the 2nd. You will have to disconnect the distributor during the process.
76 | - Test push while the application is in the background
77 |
78 | ## Development
79 |
80 | #### CI Secrets
81 | * `release_key`: keystore in base64
82 | * `release_store_password`: keystore password
83 | * `release_key_password`: key password, the key alias must be `unifiedpush`
84 | * `codeberg_token`: codeberg token for package, with `write:package` right (https://codeberg.org/user/settings/applications)
85 |
86 | # Funding
87 |
88 | This project is funded through [NGI Zero Core](https://nlnet.nl/core), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/UnifiedPush).
89 |
90 | [
](https://nlnet.nl)
91 | [
](https://nlnet.nl/core)
92 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'org.jetbrains.kotlin.plugin.compose'
5 | }
6 |
7 | android {
8 | compileSdk 35
9 |
10 | compileOptions {
11 | sourceCompatibility = 17
12 | targetCompatibility = 17
13 | }
14 |
15 | buildFeatures {
16 | compose true
17 | }
18 |
19 | defaultConfig {
20 | applicationId "org.unifiedpush.example"
21 | targetSdk 35
22 | minSdk 21
23 | versionCode 27
24 | versionName "2.0.3"
25 | multiDexEnabled true
26 |
27 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
28 | }
29 |
30 | buildTypes {
31 | release {
32 | minifyEnabled true
33 | shrinkResources true
34 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
35 | }
36 | }
37 |
38 | flavorDimensions = ["version"]
39 | productFlavors {
40 | mainFlavor {
41 | dimension "version"
42 | }
43 |
44 | fcm {
45 | dimension "version"
46 | versionNameSuffix "-fcm"
47 | }
48 | }
49 |
50 | namespace 'org.unifiedpush.example'
51 | }
52 |
53 | if (project.hasProperty('sign')) {
54 | android {
55 | signingConfigs {
56 | release {
57 | storeFile file(System.getenv("RELEASE_STORE_FILE"))
58 | storePassword System.getenv("RELEASE_STORE_PASSWORD")
59 | keyAlias System.getenv("RELEASE_KEY_ALIAS")
60 | keyPassword System.getenv("RELEASE_KEY_PASSWORD")
61 | }
62 | }
63 | }
64 | android.buildTypes.release.signingConfig android.signingConfigs.release
65 | }
66 |
67 | dependencies {
68 | ext.connector = 'org.unifiedpush.android:connector:3.0.4'
69 | ext.connector_ui = "org.unifiedpush.android:connector-ui:1.1.0"
70 | ext.embedded_distrib = 'org.unifiedpush.android:embedded-fcm-distributor:3.0.0-rc1'
71 |
72 | ext.uiTooling = "1.7.5"
73 | implementation "androidx.compose.material3:material3-android:1.3.1"
74 | implementation "androidx.compose.ui:ui-tooling-preview-android:$uiTooling"
75 | implementation "androidx.compose.ui:ui-tooling:$uiTooling"
76 | implementation "androidx.activity:activity-compose:1.9.3"
77 | implementation "com.google.accompanist:accompanist-permissions:0.36.0"
78 |
79 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
80 | mainFlavorImplementation 'androidx.appcompat:appcompat:1.7.0'
81 | fcmImplementation 'androidx.appcompat:appcompat:1.7.0'
82 | mainFlavorImplementation 'com.google.android.material:material:1.12.0'
83 | fcmImplementation 'com.google.android.material:material:1.12.0'
84 | implementation 'com.google.crypto.tink:apps-webpush:1.11.0'
85 | implementation 'com.android.volley:volley:1.2.1'
86 | implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
87 | implementation "androidx.multidex:multidex:2.0.1"
88 | implementation "androidx.work:work-runtime:2.10.0"
89 |
90 | //Flavors
91 | implementation connector //delToDevMain//
92 | //toDevMain// implementation project(':connector')
93 | implementation connector_ui //delToDevUILib//
94 | //toDevUILib// implementation project(':connector_ui')
95 |
96 | fcmImplementation(embedded_distrib) //delToDevFcm//
97 | //toDevFcm// fcmImplementation project(':distributor')
98 | }
99 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 | -dontwarn com.google.firebase.analytics.connector.AnalyticsConnector
--------------------------------------------------------------------------------
/app/src/fcm/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/fcm/google-services.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "project_info": {
4 | "project_number": "64518491375",
5 | "project_id": "unifiedpush-3f07b",
6 | "storage_bucket": "unifiedpush-3f07b.appspot.com"
7 | },
8 | "client": [
9 | {
10 | "client_info": {
11 | "mobilesdk_app_id": "1:64518491375:android:839eaf67570270e2b69f1a",
12 | "android_client_info": {
13 | "package_name": "org.unifiedpush.example"
14 | }
15 | },
16 | "oauth_client": [
17 | {
18 | "client_id": "64518491375-f8ds3h419jcbesmform13mdl2kcg0ois.apps.googleusercontent.com",
19 | "client_type": 3
20 | }
21 | ],
22 | "api_key": [
23 | {
24 | "current_key": "AIzaSyBVSqffMY71m_Jp1chpq4FpZdQ1UTtU4a0"
25 | }
26 | ],
27 | "services": {
28 | "appinvite_service": {
29 | "other_platform_oauth_client": [
30 | {
31 | "client_id": "64518491375-f8ds3h419jcbesmform13mdl2kcg0ois.apps.googleusercontent.com",
32 | "client_type": 3
33 | }
34 | ]
35 | }
36 | }
37 | }
38 | ],
39 | "configuration_version": "1"
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/fcm/java/org/unifiedpush/example/EmbeddedDistributor.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example
2 |
3 | import org.unifiedpush.android.embedded_fcm_distributor.DefaultGateway
4 | import org.unifiedpush.android.embedded_fcm_distributor.EmbeddedDistributorReceiver
5 |
6 | class EmbeddedDistributor : EmbeddedDistributorReceiver() {
7 | override val gateway = DefaultGateway
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
32 |
33 |
34 |
35 |
36 |
40 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/ApplicationServer.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.security.keystore.KeyGenParameterSpec
6 | import android.security.keystore.KeyProperties
7 | import android.util.Log
8 | import android.widget.Toast
9 | import androidx.annotation.RequiresApi
10 | import com.android.volley.NetworkResponse
11 | import com.android.volley.RequestQueue
12 | import com.android.volley.Response
13 | import com.android.volley.VolleyError
14 | import com.android.volley.toolbox.StringRequest
15 | import com.android.volley.toolbox.Volley
16 | import com.google.crypto.tink.apps.webpush.WebPushHybridEncrypt
17 | import com.google.crypto.tink.subtle.EllipticCurves
18 | import java.net.URL
19 | import java.security.KeyPair
20 | import java.security.KeyPairGenerator
21 | import java.security.KeyStore
22 | import java.security.KeyStore.PrivateKeyEntry
23 | import java.security.SecureRandom
24 | import java.security.Signature
25 | import java.security.interfaces.ECPublicKey
26 | import java.security.spec.ECGenParameterSpec
27 | import java.util.Calendar
28 | import org.json.JSONObject
29 | import org.unifiedpush.example.utils.RawRequest
30 | import org.unifiedpush.example.utils.TAG
31 | import org.unifiedpush.example.utils.b64decode
32 | import org.unifiedpush.example.utils.b64encode
33 | import org.unifiedpush.example.utils.decodePubKey
34 | import org.unifiedpush.example.utils.encode
35 | import org.unifiedpush.example.utils.vapidImplementedForSdk
36 |
37 | /**
38 | * This class emulates an application server
39 | */
40 | class ApplicationServer(private val context: Context) {
41 | private val store = Store(context)
42 |
43 | /**
44 | * Emulate notification sent from the application server to the push service.
45 | *
46 | * If the request fail, [callback] runs with the error message.
47 | */
48 | fun sendNotification(callback: (error: String?) -> Unit) {
49 | if (store.devMode && store.devCleartextTest) {
50 | sendPlainTextNotification { e ->
51 | callbackWithToasts(e, callback)
52 | }
53 | } else if (store.devMode && store.devWrongKeysTest) {
54 | sendWebPushNotification(content = "This is impossible to decrypt", fakeKeys = true) { _, e ->
55 | callbackWithToasts(e, callback)
56 | }
57 | } else {
58 | sendWebPushNotification(content = "WebPush test", fakeKeys = false) { _, e ->
59 | callbackWithToasts(e, callback)
60 | }
61 | }
62 | }
63 |
64 | private fun callbackWithToasts(e: VolleyError?, callback: (error: String?) -> Unit) {
65 | e?.let {
66 | Toast.makeText(context, "An error occurred.", Toast.LENGTH_SHORT).show()
67 | callback(it.toString())
68 | } ?: run {
69 | Toast.makeText(context, "Done", Toast.LENGTH_SHORT).show()
70 | callback(null)
71 | }
72 | }
73 |
74 | /**
75 | * Emulate notification sent from the application server to the push service with a TTL.
76 | *
77 | * If the request fail, or the response doesn't contain TTL=5 [callback] runs with the error message.
78 | */
79 | fun sendTestTTLNotification(callback: (error: String?) -> Unit) {
80 | sendWebPushNotification(content = "This must be deleted before being delivered.", fakeKeys = false) { r, e ->
81 | e?.let { return@sendWebPushNotification callback(e.toString()) }
82 | r?.let { rep ->
83 | var ttl: String?
84 | if (rep.headers?.keys?.contains("TTL") != true) {
85 | return@sendWebPushNotification callback("The response doesn't contain TTL header.")
86 | } else if (rep.headers?.get("TTL").also { ttl = it } != "5") {
87 | return@sendWebPushNotification callback("The response doesn't support TTL for 5 seconds (TTL=$ttl).")
88 | } else {
89 | return@sendWebPushNotification callback(null)
90 | }
91 | }
92 | }
93 | }
94 |
95 | /**
96 | * Emulate 2 notifications sent from the application server to the push service with the same topic.
97 | *
98 | * If the distributor is not connected, the first push message should be override.
99 | *
100 | * If the request fail [callback] runs with the error message.
101 | */
102 | fun sendTestTopicNotifications(callback: (error: String?) -> Unit) {
103 | sendWebPushNotification(
104 | content = "1st notification, it must be replaced before being delivered.",
105 | fakeKeys = false,
106 | topic = "test",
107 | ttl = 60
108 | ) { _, e1 ->
109 | e1?.let { return@sendWebPushNotification callbackWithToasts(e1, callback) }
110 | ?: run {
111 | sendWebPushNotification(
112 | content = "2nd notification, it must have replaced the previous one.",
113 | fakeKeys = false,
114 | topic = "test",
115 | ttl = 60
116 | ) { _, e2 ->
117 | callbackWithToasts(e2, callback)
118 | }
119 | }
120 | }
121 | }
122 |
123 | /**
124 | * @hide
125 | * Send plain text notifications.
126 | *
127 | * Will be used in dev mode.
128 | */
129 | private fun sendPlainTextNotification(callback: (error: VolleyError?) -> Unit) {
130 | val requestQueue: RequestQueue = Volley.newRequestQueue(context)
131 | val url = Store(context).endpoint
132 | val stringRequest: StringRequest =
133 | object :
134 | StringRequest(
135 | Method.POST,
136 | url,
137 | Response.Listener {
138 | callback(null)
139 | },
140 | Response.ErrorListener(callback)
141 | ) {
142 | override fun getParams(): MutableMap {
143 | val params = mutableMapOf()
144 | params["title"] = "Test"
145 | params["message"] = "Send in cleartext."
146 | params["priority"] = "5"
147 | return params
148 | }
149 | }
150 | requestQueue.add(stringRequest)
151 | }
152 |
153 | /**
154 | * Send a notification encrypted with RFC8291
155 | */
156 | private fun sendWebPushNotification(
157 | content: String,
158 | fakeKeys: Boolean,
159 | topic: String? = null,
160 | ttl: Int = 5,
161 | callback: (response: NetworkResponse?, error: VolleyError?) -> Unit
162 | ) {
163 | val requestQueue: RequestQueue = Volley.newRequestQueue(context)
164 | val url = Store(context).endpoint
165 | val request =
166 | object :
167 | RawRequest(
168 | Method.POST,
169 | url,
170 | Response.Listener { r ->
171 | callback(r, null)
172 | },
173 | Response.ErrorListener { e ->
174 | callback(null, e)
175 | }
176 | ) {
177 | override fun getBody(): ByteArray {
178 | val auth =
179 | if (fakeKeys) {
180 | genAuth()
181 | } else {
182 | store.b64authSecret?.b64decode()
183 | }
184 | val hybridEncrypt =
185 | WebPushHybridEncrypt.Builder()
186 | .withAuthSecret(auth)
187 | .withRecipientPublicKey(store.serializedPubKey?.decodePubKey() as ECPublicKey)
188 | .build()
189 | return hybridEncrypt.encrypt(content.toByteArray(), null)
190 | }
191 |
192 | override fun getHeaders(): Map {
193 | val params: MutableMap = HashMap()
194 | params["Content-Encoding"] = "aes128gcm"
195 | params["TTL"] = "$ttl"
196 | params["Urgency"] = if (store.devMode) store.urgency.value else Urgency.HIGH.value
197 | topic?.let {
198 | params["Topic"] = it
199 | }
200 | if (vapidImplementedForSdk() &&
201 | (
202 | (store.devMode && store.devUseVapid) ||
203 | store.distributorRequiresVapid
204 | )
205 | ) {
206 | params["Authorization"] = getVapidHeader(fakeKeys = (store.devMode && store.devWrongVapidKeysTest))
207 | }
208 | return params
209 | }
210 | }
211 | requestQueue.add(request)
212 | }
213 |
214 | private fun genAuth(): ByteArray {
215 | return ByteArray(16).apply {
216 | SecureRandom().nextBytes(this)
217 | }
218 | }
219 |
220 | /**
221 | * Emulate saving the endpoint on the application server.
222 | */
223 | fun storeEndpoint(endpoint: String?) {
224 | store.endpoint = endpoint
225 | }
226 |
227 | /**
228 | * Emulate saving the web push public keys on the application server.
229 | */
230 | fun storeWebPushKeys(auth: String, p256dh: String) {
231 | store.b64authSecret = auth
232 | store.serializedPubKey = p256dh
233 | }
234 |
235 | /**
236 | * Get the VAPID header for the endpoint, from cache or generate a new one
237 | *
238 | * - The header is cached for 5 minutes: following RFC8292, push servers should
239 | * cache the JWT to avoid checking the signature every time. This allows to test
240 | * it.
241 | * - The header is valid for 12h to allow manually testing the server from CLI
242 | *
243 | * This is for the `Authorization` header.
244 | *
245 | * @return [String] "vapid t=$JWT,k=$PUBKEY"
246 | */
247 | @RequiresApi(Build.VERSION_CODES.M)
248 | fun getVapidHeader(fakeKeys: Boolean = false): String {
249 | val endpoint = store.endpoint ?: return ""
250 | val vapidCache = vapidCache
251 | return if (
252 | !fakeKeys &&
253 | vapidCache != null &&
254 | vapidCache.endpoint == endpoint &&
255 | vapidCache.date.after(Calendar.getInstance())
256 | ) {
257 | vapidCache.value
258 | } else {
259 | genVapidHeader(endpoint, fakeKeys).also { vapid ->
260 | if (!fakeKeys) {
261 | Log.d(TAG, "Caching VAPID header: $vapid")
262 | ApplicationServer.vapidCache = VapidCache(
263 | endpoint = endpoint,
264 | date = Calendar.getInstance().apply { add(Calendar.MINUTE, 5) },
265 | value = vapid
266 | )
267 | }
268 | }
269 | }
270 | }
271 |
272 | /**
273 | * Generate VAPID header for the endpoint, valid for 12h
274 | *
275 | * This is for the `Authorization` header.
276 | *
277 | * @return [String] "vapid t=$JWT,k=$PUBKEY"
278 | */
279 | @RequiresApi(Build.VERSION_CODES.M)
280 | fun genVapidHeader(endpointStr: String, fakeKeys: Boolean = false): String {
281 | val header =
282 | JSONObject()
283 | .put("alg", "ES256")
284 | .put("typ", "JWT")
285 | .toString().toByteArray(Charsets.UTF_8)
286 | .b64encode()
287 | val endpoint = URL(endpointStr)
288 | val time12h = ((System.currentTimeMillis() / 1000) + 43200) // +12h
289 |
290 | /**
291 | * [org.json.JSONStringer#string] Doesn't follow RFC, '/' = 0x2F doesn't have to be escaped
292 | */
293 | val body =
294 | JSONObject()
295 | .put("aud", "${endpoint.protocol}://${endpoint.authority}")
296 | .put("exp", time12h)
297 | .put("sub", "https://codeberg.org/UnifiedPush/android-example")
298 | .toString()
299 | .replace("\\/", "/")
300 | .toByteArray(Charsets.UTF_8)
301 | .b64encode()
302 | val toSign = "$header.$body".toByteArray(Charsets.UTF_8)
303 | val signature =
304 | (
305 | if (fakeKeys) {
306 | signWithTempKey(toSign)
307 | } else {
308 | sign(toSign)
309 | }
310 | )?.b64encode() ?: ""
311 | val jwt = "$header.$body.$signature"
312 | return "vapid t=$jwt,k=${store.vapidPubKey}"
313 | }
314 |
315 | /**
316 | * Generate a new KeyPair for VAPID on the fake server side
317 | */
318 | @RequiresApi(Build.VERSION_CODES.M)
319 | fun genVapidKey(): KeyPair {
320 | Log.d(TAG, "Generating a new KP.")
321 | val generator =
322 | KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, KEYSTORE_PROVIDER)
323 | generator.initialize(
324 | KeyGenParameterSpec.Builder(ALIAS, KeyProperties.PURPOSE_SIGN)
325 | .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
326 | .setDigests(KeyProperties.DIGEST_SHA256)
327 | .setUserAuthenticationRequired(false)
328 | .build()
329 | )
330 | return generator.generateKeyPair().also {
331 | val pubkey = (it.public as ECPublicKey).encode()
332 | Log.d(TAG, "Pubkey: $pubkey")
333 | store.vapidPubKey = pubkey
334 | }
335 | }
336 |
337 | @RequiresApi(Build.VERSION_CODES.M)
338 | fun updateVapidKey(): KeyPair {
339 | KeyStore.getInstance(KEYSTORE_PROVIDER).apply {
340 | load(null)
341 | }.deleteEntry(ALIAS)
342 | return genVapidKey()
343 | }
344 |
345 | /**
346 | * Sign [data] using the generated VAPID key pair
347 | */
348 | @RequiresApi(Build.VERSION_CODES.M)
349 | private fun sign(data: ByteArray): ByteArray? {
350 | val ks =
351 | KeyStore.getInstance(KEYSTORE_PROVIDER).apply {
352 | load(null)
353 | }
354 | if (!ks.containsAlias(ALIAS) || !ks.entryInstanceOf(ALIAS, PrivateKeyEntry::class.java)) {
355 | // This should never be called. When we sign something, the key are already created.
356 | genVapidKey()
357 | }
358 | val entry: KeyStore.Entry = ks.getEntry(ALIAS, null)
359 | if (entry !is PrivateKeyEntry) {
360 | Log.w(TAG, "Not an instance of a PrivateKeyEntry")
361 | return null
362 | }
363 | // printX509pub(entry.certificate.publicKey)
364 | return Signature.getInstance("SHA256withECDSA").run {
365 | initSign(entry.privateKey)
366 | update(data)
367 | sign()
368 | }.let { EllipticCurves.ecdsaDer2Ieee(it, 64) }
369 | }
370 |
371 | private fun signWithTempKey(data: ByteArray): ByteArray? {
372 | val keyPair: KeyPair =
373 | EllipticCurves.generateKeyPair(EllipticCurves.CurveType.NIST_P256)
374 | // printX509pub(keyPair.public)
375 | // printX509priv(keyPair.private)
376 | return Signature.getInstance("SHA256withECDSA").run {
377 | initSign(keyPair.private)
378 | update(data)
379 | sign()
380 | }.let { EllipticCurves.ecdsaDer2Ieee(it, 64) }
381 | }
382 |
383 | /*
384 | private fun printX509pub(pubkey: PublicKey) {
385 | val b64 = Base64.encode(pubkey.encoded, Base64.DEFAULT).toString(Charsets.UTF_8)
386 | Log.d(TAG, "-----BEGIN PUBLIC KEY-----\n$b64-----END PUBLIC KEY-----")
387 | }
388 |
389 | private fun printX509priv(privkey: PrivateKey) {
390 | val b64 = Base64.encode(privkey.encoded, Base64.DEFAULT).toString(Charsets.UTF_8)
391 | Log.d(TAG, "-----BEGIN PRIVATE KEY-----\n$b64-----END PRIVATE KEY-----")
392 | }
393 | */
394 |
395 | data class VapidCache(
396 | val endpoint: String,
397 | val date: Calendar,
398 | val value: String
399 | )
400 |
401 | private companion object {
402 | private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
403 | private const val ALIAS = "ApplicationServer"
404 | private var vapidCache: VapidCache? = null
405 | }
406 | }
407 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/PushServiceImpl.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.util.Log
6 | import android.widget.Toast
7 | import org.unifiedpush.android.connector.FailedReason
8 | import org.unifiedpush.android.connector.PushService
9 | import org.unifiedpush.android.connector.UnifiedPush
10 | import org.unifiedpush.android.connector.data.PushEndpoint
11 | import org.unifiedpush.android.connector.data.PushMessage
12 | import org.unifiedpush.example.activities.Events
13 | import org.unifiedpush.example.utils.Notifier
14 | import org.unifiedpush.example.utils.TAG
15 | import org.unifiedpush.example.utils.decodeMessage
16 | import org.unifiedpush.example.utils.vapidImplementedForSdk
17 |
18 | class PushServiceImpl : PushService() {
19 | private val context = this
20 | override fun onMessage(message: PushMessage, instance: String) {
21 | val store = Store(context)
22 | if (!store.devMode) {
23 | val params = decodeMessage(message.content.toString(Charsets.UTF_8))
24 | notify(context, params)
25 | } else {
26 | // For developer mode only
27 | val params =
28 | if (store.devForceEncrypted && !message.decrypted) {
29 | mapOf(
30 | "title" to "Error",
31 | "message" to "Couldn't decrypt message.",
32 | "priority" to "8"
33 | )
34 | } else {
35 | decodeMessage(message.content.toString(Charsets.UTF_8))
36 | }
37 | notify(context, params)
38 | if (store.devStartForeground && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
39 | TestService.startForeground(context)
40 | Events.emit(Events.Type.UpdateUi)
41 | }
42 | }
43 | }
44 |
45 | private fun notify(context: Context, params: Map) {
46 | val text = params["message"] ?: "Internal error"
47 | val priority = params["priority"]?.toInt() ?: 8
48 | val title = params["title"] ?: context.getString(R.string.app_name)
49 | Notifier(context).showNotification(title, text, priority)
50 | }
51 |
52 | override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) {
53 | Log.d(TAG, "New Endpoint: ${endpoint.url}")
54 | ApplicationServer(context).storeEndpoint(endpoint.url)
55 | endpoint.pubKeySet?.let {
56 | ApplicationServer(context).storeWebPushKeys(
57 | it.auth,
58 | it.pubKey
59 | )
60 | }
61 | Events.emit(Events.Type.UpdateUi)
62 | }
63 |
64 | override fun onRegistrationFailed(reason: FailedReason, instance: String) {
65 | Toast.makeText(context, "Registration Failed: $reason", Toast.LENGTH_SHORT).show()
66 | if (reason == FailedReason.VAPID_REQUIRED) {
67 | if (vapidImplementedForSdk()) {
68 | val store = Store(context)
69 | store.distributorRequiresVapid = true
70 | ApplicationServer(context).genVapidKey()
71 | UnifiedPush.register(context, instance, vapid = store.vapidPubKey)
72 | } else {
73 | Toast.makeText(
74 | context,
75 | "Distributor requires VAPID but it isn't implemented for old Android versions.",
76 | Toast.LENGTH_SHORT
77 | ).show()
78 | UnifiedPush.removeDistributor(context)
79 | }
80 | } else {
81 | UnifiedPush.removeDistributor(context)
82 | }
83 | }
84 |
85 | override fun onUnregistered(instance: String) {
86 | // Remove the endpoint on the application server
87 | ApplicationServer(context).storeEndpoint(null)
88 | Events.emit(Events.Type.UpdateUi)
89 | val appName = context.getString(R.string.app_name)
90 | Toast.makeText(context, "$appName is unregistered", Toast.LENGTH_SHORT).show()
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/Store.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example
2 |
3 | import android.content.Context
4 | import org.unifiedpush.android.connector.UnifiedPush
5 |
6 | private const val PREF_MASTER = "org.unifiedpush.example::store"
7 | private const val PREF_DEV_MODE = "org.unifiedpush.example::store::devMode"
8 | private const val PREF_DEV_FOREGROUND_SERVICE = "org.unifiedpush.example::store::dev::foregroundService"
9 | private const val PREF_DEV_CLEARTEXT_TEST = "org.unifiedpush.example::store::dev::cleartextTest"
10 | private const val PREF_DEV_WRONG_KEYS_TEST = "org.unifiedpush.example::store::dev::wrongKeysTest"
11 | private const val PREF_DEV_WRONG_VAPID_KEYS = "org.unifiedpush.example::store::dev::wrongVapidKeysTest"
12 | private const val PREF_DEV_FORCE_ENCRYPTED = "org.unifiedpush.example::store::dev::forceEncrypted"
13 | private const val PREF_DEV_USE_VAPID = "org.unifiedpush.example::store::dev::useVapid"
14 | private const val PREF_ENDPOINT = "org.unifiedpush.example::store::endpoint"
15 | private const val PREF_URGENCY = "org.unifiedpush.example::store::urgency"
16 | private const val PREF_PUBKEY = "org.unifiedpush.example::store::pubkey"
17 | private const val PREF_AUTHKEY = "org.unifiedpush.example::store::authkey"
18 | private const val PREF_DISTRIB_REQ_VAPID = "org.unifiedpush.example::store::distribRequiresVapid"
19 | private const val PREF_VAPID_PUBKEY = "org.unifiedpush.example::store::vapidPubKey"
20 |
21 | /**
22 | * Class containing stored parameters and values.
23 | */
24 | class Store(val context: Context) {
25 | private val prefs = context.getSharedPreferences(PREF_MASTER, Context.MODE_PRIVATE)
26 |
27 | /** Is Developer mode enabled. */
28 | var devMode: Boolean
29 | get() = prefs.getBoolean(PREF_DEV_MODE, false)
30 | set(value) = prefs.edit().putBoolean(PREF_DEV_MODE, value).apply()
31 |
32 | /** For developer mode: Start foreground service when a push message is received. */
33 | var devStartForeground: Boolean
34 | get() = prefs.getBoolean(PREF_DEV_FOREGROUND_SERVICE, false)
35 | set(value) = prefs.edit().putBoolean(PREF_DEV_FOREGROUND_SERVICE, value).apply()
36 |
37 | /** For developer mode: Send unencrypted push messages. */
38 | var devCleartextTest: Boolean
39 | get() = prefs.getBoolean(PREF_DEV_CLEARTEXT_TEST, false)
40 | set(value) = prefs.edit().putBoolean(PREF_DEV_CLEARTEXT_TEST, value).apply()
41 |
42 | /** For developer mode: Use wrong encryption key to send push messages. */
43 | var devWrongKeysTest: Boolean
44 | get() = prefs.getBoolean(PREF_DEV_WRONG_KEYS_TEST, false)
45 | set(value) = prefs.edit().putBoolean(PREF_DEV_WRONG_KEYS_TEST, value).apply()
46 |
47 | /** For developer mode: Use wrong VAPID key with push messages.*/
48 | var devWrongVapidKeysTest: Boolean
49 | get() = prefs.getBoolean(PREF_DEV_WRONG_VAPID_KEYS, false)
50 | set(value) = prefs.edit().putBoolean(PREF_DEV_WRONG_VAPID_KEYS, value).apply()
51 |
52 | /**
53 | * For developer mode: Show an error notification when receiving an unencrypted push message.
54 | *
55 | * When [PushMessage.decrypted][org.unifiedpush.android.connector.data.PushMessage.decrypted]
56 | * is false.
57 | */
58 | var devForceEncrypted: Boolean
59 | get() = prefs.getBoolean(PREF_DEV_FORCE_ENCRYPTED, false)
60 | set(value) = prefs.edit().putBoolean(PREF_DEV_FORCE_ENCRYPTED, value).apply()
61 |
62 | /** For developer mode: Use VAPID even if the distributor doesn't require it. */
63 | var devUseVapid: Boolean
64 | get() = prefs.getBoolean(PREF_DEV_USE_VAPID, false)
65 | set(value) = prefs.edit().putBoolean(PREF_DEV_USE_VAPID, value).apply()
66 |
67 | /** Push endpoint. Should be saved on application server. */
68 | var endpoint: String?
69 | get() = UnifiedPush.getAckDistributor(context)?.let { prefs.getString(PREF_ENDPOINT, null) }
70 | set(value) {
71 | if (value == null) {
72 | prefs.edit().remove(PREF_ENDPOINT).apply()
73 | } else {
74 | prefs.edit().putString(PREF_ENDPOINT, value).apply()
75 | }
76 | }
77 |
78 | /** For developer mode: Urgency for push messages. Should be chosen by the application server. */
79 | var urgency: Urgency
80 | get() = Urgency.fromValue(prefs.getString(PREF_URGENCY, null))
81 | set(value) = prefs.edit().putString(PREF_URGENCY, value.value).apply()
82 |
83 | /** WebPush public key. Should be saved on application server. */
84 | var serializedPubKey: String?
85 | get() = prefs.getString(PREF_PUBKEY, null)
86 | set(value) = prefs.edit().putString(PREF_PUBKEY, value).apply()
87 |
88 | /** Does the distributor requires VAPID ? It should always be used if the application server supports it. */
89 | var distributorRequiresVapid: Boolean
90 | get() = prefs.getBoolean(PREF_DISTRIB_REQ_VAPID, false)
91 | set(value) = prefs.edit().putBoolean(PREF_DISTRIB_REQ_VAPID, value).apply()
92 |
93 | /** VAPID public key. Should be saved on application server. */
94 | var vapidPubKey: String?
95 | get() = prefs.getString(PREF_VAPID_PUBKEY, null)
96 | set(value) = prefs.edit().putString(PREF_VAPID_PUBKEY, value).apply()
97 |
98 | /** WebPush auth secret. Should be saved on application server. */
99 | var b64authSecret: String?
100 | get() = prefs.getString(PREF_AUTHKEY, null)
101 | set(value) = prefs.edit().putString(PREF_AUTHKEY, value).apply()
102 | }
103 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/TestService.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example
2 |
3 | import android.app.Notification
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.app.Service
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.os.Build
10 | import android.os.IBinder
11 | import android.util.Log
12 | import androidx.annotation.RequiresApi
13 | import kotlin.concurrent.Volatile
14 | import org.unifiedpush.example.activities.Events
15 |
16 | /**
17 | * Service to test [foreground services](https://developer.android.com/develop/background-work/services/foreground-services) started from the background.
18 | *
19 | * It isn't possible to start a foreground service from the background except if:
20 | * - the application is unrestricted for battery use
21 | * - the application has been set in saving power whitelist for some time (this happens after a FCM urgent push message)
22 | * - the application is in foreground importance (a distributor can bring an application to foreground important by binding to the connector dedicated service)
23 | */
24 | class TestService : Service() {
25 | override fun onBind(intent: Intent?): IBinder? {
26 | Log.d(TAG, "Bound")
27 | return null
28 | }
29 |
30 | override fun onCreate() {
31 | super.onCreate()
32 | Log.i(TAG, "Created")
33 | createNotificationChannel()
34 | val notification = createNotification()
35 | startForeground(NOTIFICATION_ID_FOREGROUND, notification)
36 | }
37 |
38 | override fun onDestroy() {
39 | super.onDestroy()
40 | Log.i(TAG, "Destroyed")
41 | }
42 |
43 | private fun createNotificationChannel() {
44 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
45 | val notificationManager =
46 | this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
47 | val channel =
48 | NotificationChannel(
49 | CHANNEL_ID,
50 | "TestService",
51 | NotificationManager.IMPORTANCE_HIGH
52 | ).let {
53 | it.description = "test"
54 | it
55 | }
56 | notificationManager.createNotificationChannel(channel)
57 | }
58 | }
59 |
60 | private fun createNotification(): Notification {
61 | val builder: Notification.Builder =
62 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
63 | Notification.Builder(
64 | this,
65 | CHANNEL_ID
66 | )
67 | } else {
68 | Notification.Builder(this)
69 | }
70 |
71 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
72 | builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
73 | }
74 |
75 | return builder
76 | .setContentTitle("TestService")
77 | .setContentText("Run in foreground")
78 | .setSmallIcon(R.drawable.ic_launcher_foreground)
79 | .setTicker("foo")
80 | .setOngoing(true)
81 | .build()
82 | }
83 |
84 | companion object {
85 | /** Start [TestService] in the foreground */
86 | @RequiresApi(Build.VERSION_CODES.O)
87 | fun startForeground(context: Context) {
88 | synchronized(lock) {
89 | val intent = Intent(context, TestService::class.java)
90 | context.startForegroundService(intent)
91 | started = true
92 | }
93 | Events.emit(Events.Type.UpdateUi)
94 | }
95 |
96 | /** Stop [TestService] */
97 | fun stop(context: Context) {
98 | synchronized(lock) {
99 | val intent = Intent(context, TestService::class.java)
100 | context.stopService(intent)
101 | started = false
102 | }
103 | Events.emit(Events.Type.UpdateUi)
104 | }
105 |
106 | /** Is the foreground test service running ? */
107 | fun isStarted(): Boolean = synchronized(lock) {
108 | started
109 | }
110 |
111 | @Volatile
112 | private var started = false
113 | private var lock = Object()
114 | private const val CHANNEL_ID = "TestService:ChannelId"
115 | private const val NOTIFICATION_ID_FOREGROUND = 0x1000
116 | private const val TAG = "TestService"
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/Tests.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example
2 |
3 | import android.app.Activity
4 | import android.widget.Toast
5 | import androidx.appcompat.app.AlertDialog
6 | import java.util.Timer
7 | import kotlin.concurrent.schedule
8 | import org.unifiedpush.example.utils.DelayedRequestWorker
9 |
10 | class Tests(private val activity: Activity) {
11 | fun testTTL(callback: (error: String?) -> Unit) {
12 | testTTLIntro {
13 | ApplicationServer(activity).sendTestTTLNotification { e ->
14 | Toast.makeText(activity, "Notification sent.", Toast.LENGTH_SHORT).show()
15 | e?. run {
16 | Toast.makeText(activity, "TTL not (fully) supported.", Toast.LENGTH_SHORT).show()
17 | } ?: run {
18 | Thread.dumpStack()
19 | Timer().schedule(10_000L) {
20 | activity.runOnUiThread {
21 | Toast.makeText(
22 | activity,
23 | "You can reconnect your distributor.",
24 | Toast.LENGTH_SHORT
25 | ).show()
26 | }
27 | }
28 | }
29 | callback(e)
30 | }
31 | }
32 | }
33 |
34 | private fun testTTLIntro(onSuccess: () -> Unit) {
35 | val builder: AlertDialog.Builder = AlertDialog.Builder(activity)
36 | builder.setTitle("Testing TTL")
37 | builder.setMessage(
38 | "To check the TTL, you must first disconnect your distributor.\n" +
39 | "You can reconnect it after 10 seconds.\n" +
40 | "A notification will be sent, it should not be displayed by the application.\n\n" +
41 | "Press OK once the distributor is disconnected."
42 | )
43 | builder.setPositiveButton(android.R.string.ok) { _, _ ->
44 | onSuccess()
45 | }
46 | builder.setNegativeButton(android.R.string.cancel) { _, _ ->
47 | Toast.makeText(activity, "Aborting", Toast.LENGTH_SHORT).show()
48 | }
49 | builder.create().show()
50 | }
51 |
52 | fun testTopic(callback: (error: String?) -> Unit) {
53 | testTopicIntro {
54 | ApplicationServer(activity).sendTestTopicNotifications { e ->
55 | Toast.makeText(activity, "Notifications sent.", Toast.LENGTH_SHORT).show()
56 | e ?: run {
57 | Thread.dumpStack()
58 | Timer().schedule(10_000L) {
59 | activity.runOnUiThread {
60 | Toast.makeText(
61 | activity,
62 | "You can reconnect your distributor.",
63 | Toast.LENGTH_SHORT
64 | ).show()
65 | }
66 | }
67 | }
68 | callback(e)
69 | }
70 | }
71 | }
72 |
73 | private fun testTopicIntro(onSuccess: () -> Unit) {
74 | val builder: AlertDialog.Builder = AlertDialog.Builder(activity)
75 | builder.setTitle("Testing Topic")
76 | builder.setMessage(
77 | "To check topics, you must first disconnect your distributor.\n" +
78 | "You can reconnect it after 10 seconds.\n" +
79 | "2 notifications will be sent, only the 2nd one should be displayed by the application.\n\n" +
80 | "If you see 2 notifications, your distributor doesn't support notification update.\n\n" +
81 | "Press OK once the distributor is disconnected."
82 | )
83 | builder.setPositiveButton(android.R.string.ok) { _, _ ->
84 | onSuccess()
85 | }
86 | builder.setNegativeButton(android.R.string.cancel) { _, _ ->
87 | Toast.makeText(activity, "Aborting", Toast.LENGTH_SHORT).show()
88 | }
89 | builder.create().show()
90 | }
91 |
92 | /**
93 | * Display introduction for test sending a notification while the app is in background.
94 | *
95 | * Calls [testMessageInBackgroundIntro] and set [runBackgroundCheck] to `true`.
96 | *
97 | * It requires the user to put the application in background and [testMessageInBackgroundRun]
98 | * to be called in [Activity.onPause].
99 | */
100 | fun testMessageInBackgroundStart() {
101 | testMessageInBackgroundIntro { runBackgroundCheck = true }
102 | }
103 |
104 | private fun testMessageInBackgroundIntro(onSuccess: () -> Unit) {
105 | val builder: AlertDialog.Builder = AlertDialog.Builder(activity)
106 | builder.setTitle("Testing notifications in background")
107 | builder.setMessage(
108 | "To check, you need to put this application in the background.\n" +
109 | "A notification will be sent after 7 seconds.\n\n" +
110 | "It's also possible to put the application in the background and send unencrypted " +
111 | "POST message to the endpoint via a terminal to test foreground services.\n\n" +
112 | "Press OK to continue."
113 | )
114 | builder.setPositiveButton(android.R.string.ok) { _, _ ->
115 | Toast.makeText(activity, "Notification will be sent in the background", Toast.LENGTH_SHORT).show()
116 | onSuccess()
117 | }
118 | builder.setNegativeButton(android.R.string.cancel) { _, _ ->
119 | Toast.makeText(activity, "Aborting", Toast.LENGTH_SHORT).show()
120 | }
121 | builder.create().show()
122 | }
123 |
124 | /**
125 | * Send a notification after 5 seconds.
126 | *
127 | * Should be called in [Activity.onPause]
128 | */
129 | fun testMessageInBackgroundRun() {
130 | if (runBackgroundCheck) {
131 | runBackgroundCheck = false
132 | DelayedRequestWorker.enqueue(activity, 7_000L)
133 | }
134 | }
135 |
136 | private companion object {
137 | var runBackgroundCheck = false
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/Urgency.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example
2 |
3 | /**
4 | * Define in [RFC8030](https://www.rfc-editor.org/rfc/rfc8030#section-5.3)
5 | * +----------+-----------------------------+--------------------------+
6 | * | Urgency | Device State | Example Application |
7 | * | | | Scenario |
8 | * +----------+-----------------------------+--------------------------+
9 | * | very-low | On power and Wi-Fi | Advertisements |
10 | * | low | On either power or Wi-Fi | Topic updates |
11 | * | normal | On neither power nor Wi-Fi | Chat or Calendar Message |
12 | * | high | Low battery | Incoming phone call or |
13 | * | | | time-sensitive alert |
14 | * +----------+-----------------------------+--------------------------+
15 | */
16 | enum class Urgency(val value: String) {
17 | /** On power and Wi-Fi, example: Advertisements */
18 | VERY_LOW("very-low"),
19 |
20 | /** On either power or Wi-Fi, example: Topic updates */
21 | LOW("low"),
22 |
23 | /** On neither power nor Wi-Fi, example: Chat or Calendar Message */
24 | NORMAL("normal"),
25 |
26 | /** Low battery, example: Incoming phone call or time-sensitive alert */
27 | HIGH("high") ;
28 |
29 | companion object {
30 | fun fromValue(s: String?): Urgency {
31 | return Urgency.entries.find { it.value == s } ?: Urgency.NORMAL
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/AppBarViewModel.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.ViewModel
8 | import org.unifiedpush.example.Store
9 | import org.unifiedpush.example.activities.ui.AppBarUiState
10 |
11 | class AppBarViewModel(
12 | uiState: AppBarUiState,
13 | context: Context? = null
14 | ) : ViewModel() {
15 |
16 | var uiState by mutableStateOf(uiState)
17 | private set
18 |
19 | private var store: Store? = context?.let { Store(context) }
20 |
21 | constructor(context: Context) : this(
22 | uiState = AppBarUiState.from(context),
23 | context = context
24 | )
25 |
26 | fun toggleDevMode() {
27 | uiState = uiState.copy(devMode = !uiState.devMode)
28 | store?.devMode = uiState.devMode
29 | Events.emit(Events.Type.UpdateUi)
30 | }
31 |
32 | fun toggleErrorIfDecryptionFail() {
33 | uiState = uiState.copy(errorIfDecryptionFail = !uiState.errorIfDecryptionFail)
34 | store?.devForceEncrypted = uiState.errorIfDecryptionFail
35 | }
36 |
37 | fun toggleUseVapid() {
38 | uiState = uiState.copy(useVapid = !uiState.useVapid)
39 | store?.devUseVapid = uiState.useVapid
40 | Events.emit(Events.Type.UpdateUi)
41 | }
42 |
43 | fun toggleSendClearTextTests() {
44 | uiState = uiState.copy(sendClearTextTests = !uiState.sendClearTextTests)
45 | store?.devCleartextTest = uiState.sendClearTextTests
46 | Events.emit(Events.Type.UpdateUi)
47 | }
48 |
49 | fun toggleUseWrongVapidKeys() {
50 | uiState = uiState.copy(useWrongVapidKeys = !uiState.useWrongVapidKeys)
51 | store?.devWrongVapidKeysTest = uiState.useWrongVapidKeys
52 | }
53 |
54 | fun toggleUseWrongEncryptionKeys() {
55 | uiState = uiState.copy(useWrongEncryptionKeys = !uiState.useWrongEncryptionKeys)
56 | store?.devWrongKeysTest = uiState.useWrongEncryptionKeys
57 | Events.emit(Events.Type.UpdateUi)
58 | }
59 |
60 | fun toggleStartForegroundServiceOnMessage() {
61 | uiState = uiState.copy(startForegroundServiceOnMessage = !uiState.startForegroundServiceOnMessage)
62 | store?.devStartForeground = uiState.startForegroundServiceOnMessage
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/CheckActivity.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.util.Log
7 | import android.widget.Toast
8 | import androidx.activity.ComponentActivity
9 | import androidx.activity.compose.setContent
10 | import kotlinx.coroutines.Job
11 | import org.unifiedpush.android.connector.LinkActivityHelper
12 | import org.unifiedpush.android.connector.UnifiedPush
13 | import org.unifiedpush.example.ApplicationServer
14 | import org.unifiedpush.example.TestService
15 | import org.unifiedpush.example.Tests
16 | import org.unifiedpush.example.activities.ui.CheckUi
17 | import org.unifiedpush.example.activities.ui.theme.AppTheme
18 | import org.unifiedpush.example.utils.RegistrationDialogs
19 | import org.unifiedpush.example.utils.TAG
20 | import org.unifiedpush.example.utils.vapidImplementedForSdk
21 |
22 | class CheckActivity : ComponentActivity() {
23 | private lateinit var appBarViewModel: AppBarViewModel
24 | private lateinit var checkViewModel: CheckViewModel
25 | private val helper = LinkActivityHelper(this)
26 | private var job: Job? = null
27 |
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 | appBarViewModel = AppBarViewModel(this)
31 | checkViewModel = CheckViewModel(this)
32 |
33 | job = Events.registerForEvents { onEvent(it) }
34 |
35 | setContent {
36 | AppTheme {
37 | CheckUi(appBarViewModel, checkViewModel)
38 | }
39 | }
40 | }
41 |
42 | override fun onResume() {
43 | super.onResume()
44 | updateUi()
45 | }
46 |
47 | override fun onDestroy() {
48 | job?.cancel()
49 | job = null
50 | super.onDestroy()
51 | }
52 |
53 | override fun onPause() {
54 | super.onPause()
55 | Tests(this).testMessageInBackgroundRun()
56 | }
57 |
58 | /**
59 | * Receive link activity result.
60 | *
61 | * If the link succeed, we register our app.
62 | */
63 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
64 | super.onActivityResult(requestCode, resultCode, data)
65 | val success = helper.onLinkActivityResult(requestCode, resultCode, data)
66 | Log.d(TAG, "Distributor found=$success")
67 | if (success) {
68 | UnifiedPush.register(this)
69 | }
70 | }
71 |
72 | private fun onEvent(type: Events.Type) {
73 | runOnUiThread {
74 | when (type) {
75 | Events.Type.UpdateUi -> updateUi()
76 | Events.Type.Unregister -> unregister()
77 | Events.Type.SendNotification -> sendNotification()
78 | Events.Type.DeepLink -> deepLink()
79 | Events.Type.Reregister -> reRegister()
80 | Events.Type.StopForegroundService -> stopForegroundService()
81 | Events.Type.ChangeDistributor -> changeDistributor()
82 | Events.Type.TestTopic -> testTopic()
83 | Events.Type.UpdateVapidKey -> updateVapidKey()
84 | Events.Type.TestInBackground -> testInBackground()
85 | Events.Type.TestTTL -> testTTL()
86 | else -> {}
87 | }
88 | }
89 | }
90 |
91 | private fun updateUi() {
92 | if (!checkViewModel.refresh(this)) {
93 | MainActivity.goToMainActivity(this)
94 | finish()
95 | }
96 | }
97 |
98 | private fun unregister() {
99 | Toast.makeText(this, "Unregistering", Toast.LENGTH_SHORT).show()
100 | ApplicationServer(this).storeEndpoint(null)
101 | UnifiedPush.unregister(this)
102 | UnifiedPush.removeDistributor(this)
103 | updateUi()
104 | }
105 |
106 | private fun sendNotification() {
107 | ApplicationServer(this).sendNotification { checkViewModel.setError(it) }
108 | }
109 |
110 | private fun deepLink() {
111 | /**
112 | * We use the [LinkActivityHelper] with [onActivityResult], but we could
113 | * also use [UnifiedPush.tryUseDefaultDistributor] directly:
114 | *
115 | * ```
116 | * UnifiedPush.tryUseDefaultDistributor(this) { success ->
117 | * Log.d(TAG, "Distributor found=$success")
118 | * }
119 | * ```
120 | */
121 | if (!helper.startLinkActivityForResult()) {
122 | Log.d(TAG, "No distributor found")
123 | }
124 | }
125 |
126 | private fun reRegister() {
127 | RegistrationDialogs(this, mayUseCurrent = true, mayUseDefault = true).run()
128 | Toast.makeText(applicationContext, "Registration sent.", Toast.LENGTH_SHORT).show()
129 | }
130 |
131 | private fun stopForegroundService() {
132 | TestService.stop(this)
133 | updateUi()
134 | }
135 |
136 | private fun changeDistributor() {
137 | RegistrationDialogs(this, mayUseCurrent = false, mayUseDefault = false).run()
138 | }
139 |
140 | private fun testTopic() {
141 | Tests(this).testTopic { checkViewModel.setError(it) }
142 | }
143 |
144 | private fun updateVapidKey() {
145 | if (vapidImplementedForSdk()) {
146 | ApplicationServer(this).updateVapidKey()
147 | updateUi()
148 | }
149 | }
150 |
151 | private fun testInBackground() {
152 | Tests(this).testMessageInBackgroundStart()
153 | }
154 |
155 | private fun testTTL() {
156 | Tests(this).testTTL { checkViewModel.setError(it) }
157 | }
158 |
159 | companion object {
160 | fun goToCheckActivity(context: Context) {
161 | Log.d(TAG, "Go to CheckActivity")
162 | val intent =
163 | Intent(
164 | context,
165 | CheckActivity::class.java
166 | )
167 | context.startActivity(intent)
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/CheckViewModel.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.ViewModel
8 | import org.unifiedpush.example.Store
9 | import org.unifiedpush.example.Urgency
10 | import org.unifiedpush.example.activities.ui.CheckUiState
11 |
12 | class CheckViewModel(
13 | uiState: CheckUiState,
14 | context: Context? = null
15 | ) : ViewModel() {
16 |
17 | var uiState by mutableStateOf(uiState)
18 | private set
19 |
20 | private var store: Store? = context?.let { Store(context) }
21 |
22 | constructor(context: Context) : this(
23 | CheckUiState.from(context) ?: CheckUiState.error(),
24 | context
25 | )
26 |
27 | /**
28 | * @return `true` if the app is still connected to the distributor
29 | */
30 | fun refresh(context: Context): Boolean {
31 | return CheckUiState.from(context)?.let {
32 | uiState = it
33 | } != null
34 | }
35 |
36 | fun setError(error: String?) {
37 | uiState = uiState.copy(error = error)
38 | }
39 |
40 | fun setUrgency(urgency: Urgency) {
41 | uiState = uiState.copy(urgency = urgency)
42 | store?.urgency = urgency
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/Events.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.Job
6 | import kotlinx.coroutines.flow.MutableSharedFlow
7 | import kotlinx.coroutines.flow.asSharedFlow
8 | import kotlinx.coroutines.flow.collectLatest
9 | import kotlinx.coroutines.launch
10 |
11 | object Events {
12 | fun registerForEvents(handler: (Type) -> Unit): Job {
13 | return CoroutineScope(Dispatchers.IO).launch {
14 | events.collectLatest { event ->
15 | handler(event)
16 | }
17 | }
18 | }
19 |
20 | suspend inline fun emitAsync(type: Type) {
21 | mutEvents.emit(type)
22 | }
23 |
24 | fun emit(type: Type) {
25 | CoroutineScope(Dispatchers.IO).launch {
26 | emitAsync(type)
27 | }
28 | }
29 |
30 | enum class Type {
31 | Register,
32 | UpdateUi,
33 | Unregister,
34 | SendNotification,
35 | DeepLink,
36 | Reregister,
37 | StopForegroundService,
38 |
39 | // SetUrgency,
40 | ChangeDistributor,
41 | TestTopic,
42 | UpdateVapidKey,
43 | TestInBackground,
44 | TestTTL
45 | }
46 |
47 | val mutEvents: MutableSharedFlow = MutableSharedFlow()
48 | private val events = mutEvents.asSharedFlow()
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.util.Log
7 | import androidx.activity.ComponentActivity
8 | import androidx.activity.compose.setContent
9 | import kotlinx.coroutines.Job
10 | import org.unifiedpush.example.Store
11 | import org.unifiedpush.example.activities.CheckActivity.Companion.goToCheckActivity
12 | import org.unifiedpush.example.activities.ui.MainUi
13 | import org.unifiedpush.example.activities.ui.theme.AppTheme
14 | import org.unifiedpush.example.utils.RegistrationDialogs
15 | import org.unifiedpush.example.utils.TAG
16 |
17 | class MainActivity : ComponentActivity() {
18 | private lateinit var appBarViewModel: AppBarViewModel
19 | private var job: Job? = null
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 | appBarViewModel = AppBarViewModel(this)
24 |
25 | job = Events.registerForEvents { onEvent(it) }
26 |
27 | setContent {
28 | AppTheme {
29 | MainUi(appBarViewModel)
30 | }
31 | }
32 | }
33 |
34 | override fun onResume() {
35 | super.onResume()
36 | val store = Store(this)
37 | if (store.endpoint != null) {
38 | goToCheckActivity(this)
39 | finish()
40 | } else {
41 | // We reset the value
42 | store.distributorRequiresVapid = false
43 | }
44 | }
45 |
46 | override fun onDestroy() {
47 | job?.cancel()
48 | job = null
49 | super.onDestroy()
50 | }
51 |
52 | private fun onEvent(type: Events.Type) {
53 | when (type) {
54 | Events.Type.Register -> {
55 | runOnUiThread {
56 | RegistrationDialogs(this, mayUseCurrent = true, mayUseDefault = true).run()
57 | }
58 | }
59 | Events.Type.UpdateUi -> {
60 | goToCheckActivity(this)
61 | finish()
62 | }
63 | else -> {}
64 | }
65 | }
66 |
67 | companion object {
68 | fun goToMainActivity(context: Context) {
69 | Log.d(TAG, "Go to MainActivity")
70 | val intent =
71 | Intent(
72 | context,
73 | MainActivity::class.java
74 | )
75 | context.startActivity(intent)
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/ui/AppBarUi.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities.ui
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.filled.MoreVert
7 | import androidx.compose.material3.Checkbox
8 | import androidx.compose.material3.DropdownMenu
9 | import androidx.compose.material3.DropdownMenuItem
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.IconButton
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Text
15 | import androidx.compose.material3.TopAppBar
16 | import androidx.compose.material3.TopAppBarDefaults
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.runtime.mutableStateOf
20 | import androidx.compose.runtime.remember
21 | import androidx.compose.runtime.setValue
22 | import androidx.compose.ui.Alignment
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.res.stringResource
25 | import androidx.compose.ui.tooling.preview.Preview
26 | import org.unifiedpush.example.R
27 | import org.unifiedpush.example.activities.AppBarViewModel
28 |
29 | @OptIn(ExperimentalMaterial3Api::class)
30 | @Composable
31 | fun AppBarUi(viewModel: AppBarViewModel) {
32 | var expanded by remember { mutableStateOf(false) }
33 |
34 | TopAppBar(
35 | colors = TopAppBarDefaults
36 | .topAppBarColors(
37 | containerColor = MaterialTheme.colorScheme.primaryContainer,
38 | titleContentColor = MaterialTheme.colorScheme.primary
39 | ),
40 | title = {
41 | Text(
42 | stringResource(R.string.app_name)
43 | )
44 | },
45 | actions = {
46 | IconButton(
47 | onClick = {
48 | expanded = !expanded
49 | }
50 | ) {
51 | Icon(
52 | imageVector = Icons.Default.MoreVert,
53 | contentDescription = "Actions"
54 | )
55 | }
56 | Dropdown(
57 | expanded,
58 | viewModel
59 | ) {
60 | expanded = false
61 | }
62 | }
63 | )
64 | }
65 |
66 | @Composable
67 | fun Dropdown(expanded: Boolean, viewModel: AppBarViewModel, onDismissRequest: () -> Unit) {
68 | DropdownMenu(
69 | expanded = expanded,
70 | onDismissRequest = { onDismissRequest() }
71 | ) {
72 | DropdownMenuItem(
73 | text = {
74 | Row(verticalAlignment = Alignment.CenterVertically) {
75 | Text("Developer mode")
76 | Spacer(Modifier.weight(1f))
77 | Checkbox(
78 | viewModel.uiState.devMode,
79 | onCheckedChange = { viewModel.toggleDevMode() }
80 | )
81 | }
82 | },
83 | onClick = { viewModel.toggleDevMode() }
84 | )
85 | if (viewModel.uiState.devMode) {
86 | DropdownMenuItem(
87 | text = {
88 | Row(verticalAlignment = Alignment.CenterVertically) {
89 | Text(
90 | "Error if decryption fails"
91 | )
92 | Spacer(Modifier.weight(1f))
93 | Checkbox(
94 | viewModel.uiState.errorIfDecryptionFail,
95 | onCheckedChange = { viewModel.toggleErrorIfDecryptionFail() }
96 | )
97 | }
98 | },
99 | onClick = { viewModel.toggleErrorIfDecryptionFail() }
100 | )
101 | DropdownMenuItem(
102 | text = {
103 | Row(verticalAlignment = Alignment.CenterVertically) {
104 | Text(
105 | "Use VAPID"
106 | )
107 | Spacer(Modifier.weight(1f))
108 | Checkbox(
109 | viewModel.uiState.useVapid,
110 | onCheckedChange = { viewModel.toggleUseVapid() }
111 | )
112 | }
113 | },
114 | onClick = { viewModel.toggleUseVapid() }
115 | )
116 | DropdownMenuItem(
117 | text = {
118 | Row(verticalAlignment = Alignment.CenterVertically) {
119 | Text(
120 | "Send cleartext test"
121 | )
122 | Spacer(Modifier.weight(1f))
123 | Checkbox(
124 | viewModel.uiState.sendClearTextTests,
125 | onCheckedChange = { viewModel.toggleSendClearTextTests() }
126 | )
127 | }
128 | },
129 | onClick = { viewModel.toggleSendClearTextTests() }
130 | )
131 | DropdownMenuItem(
132 | enabled = viewModel.uiState.useVapid,
133 | text = {
134 | Row(verticalAlignment = Alignment.CenterVertically) {
135 | Text(
136 | "Use wrong VAPID keys"
137 | )
138 | Spacer(Modifier.weight(1f))
139 | Checkbox(
140 | viewModel.uiState.useVapid &&
141 | viewModel.uiState.useWrongVapidKeys,
142 | onCheckedChange = { viewModel.toggleUseWrongVapidKeys() }
143 | )
144 | }
145 | },
146 | onClick = { viewModel.toggleUseWrongVapidKeys() }
147 | )
148 | DropdownMenuItem(
149 | enabled = !viewModel.uiState.sendClearTextTests,
150 | text = {
151 | Row(verticalAlignment = Alignment.CenterVertically) {
152 | Text(
153 | "Use wrong encryption keys"
154 | )
155 | Spacer(Modifier.weight(1f))
156 | Checkbox(
157 | !viewModel.uiState.sendClearTextTests &&
158 | viewModel.uiState.useWrongEncryptionKeys,
159 | onCheckedChange = { viewModel.toggleUseWrongEncryptionKeys() }
160 | )
161 | }
162 | },
163 | onClick = { viewModel.toggleUseWrongEncryptionKeys() }
164 | )
165 | DropdownMenuItem(
166 | text = {
167 | Row(verticalAlignment = Alignment.CenterVertically) {
168 | Text(
169 | "Fg service on message"
170 | )
171 | Spacer(Modifier.weight(1f))
172 | Checkbox(
173 | viewModel.uiState.startForegroundServiceOnMessage,
174 | onCheckedChange = { viewModel.toggleStartForegroundServiceOnMessage() }
175 | )
176 | }
177 | },
178 | onClick = { viewModel.toggleStartForegroundServiceOnMessage() }
179 | )
180 | }
181 | }
182 | }
183 |
184 | @Preview
185 | @Composable
186 | fun AppBarPreview() {
187 | AppBarUi(
188 | AppBarViewModel(
189 | uiState = AppBarUiState(
190 | devMode = true,
191 | errorIfDecryptionFail = true,
192 | useVapid = true,
193 | sendClearTextTests = true,
194 | useWrongVapidKeys = true,
195 | useWrongEncryptionKeys = true,
196 | startForegroundServiceOnMessage = true
197 | )
198 | )
199 | )
200 | }
201 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/ui/AppBarUiState.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities.ui
2 |
3 | import android.content.Context
4 | import org.unifiedpush.example.Store
5 |
6 | data class AppBarUiState(
7 | val devMode: Boolean,
8 | val errorIfDecryptionFail: Boolean,
9 | val useVapid: Boolean,
10 | val sendClearTextTests: Boolean,
11 | val useWrongVapidKeys: Boolean,
12 | val useWrongEncryptionKeys: Boolean,
13 | val startForegroundServiceOnMessage: Boolean
14 | ) {
15 | companion object {
16 | fun from(context: Context): AppBarUiState {
17 | val store = Store(context)
18 | return AppBarUiState(
19 | devMode = store.devMode,
20 | errorIfDecryptionFail = store.devForceEncrypted,
21 | useVapid = store.devUseVapid,
22 | sendClearTextTests = store.devCleartextTest,
23 | useWrongVapidKeys = store.devWrongVapidKeysTest,
24 | useWrongEncryptionKeys = store.devWrongKeysTest,
25 | startForegroundServiceOnMessage = store.devStartForeground
26 | )
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/ui/CheckUi.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities.ui
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.rememberScrollState
11 | import androidx.compose.foundation.text.selection.SelectionContainer
12 | import androidx.compose.foundation.verticalScroll
13 | import androidx.compose.material3.Button
14 | import androidx.compose.material3.HorizontalDivider
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Scaffold
17 | import androidx.compose.material3.Text
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.getValue
20 | import androidx.compose.runtime.mutableStateOf
21 | import androidx.compose.runtime.remember
22 | import androidx.compose.runtime.setValue
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.text.LinkAnnotation
26 | import androidx.compose.ui.text.SpanStyle
27 | import androidx.compose.ui.text.TextLinkStyles
28 | import androidx.compose.ui.text.buildAnnotatedString
29 | import androidx.compose.ui.text.style.TextOverflow
30 | import androidx.compose.ui.text.withLink
31 | import androidx.compose.ui.tooling.preview.Preview
32 | import androidx.compose.ui.unit.dp
33 | import org.unifiedpush.example.Urgency
34 | import org.unifiedpush.example.activities.AppBarViewModel
35 | import org.unifiedpush.example.activities.CheckViewModel
36 | import org.unifiedpush.example.activities.Events
37 | import org.unifiedpush.example.utils.genTestPageUrl
38 |
39 | @Composable
40 | fun CheckUi(appBarViewModel: AppBarViewModel, viewModel: CheckViewModel) {
41 | Scaffold(
42 | topBar = { AppBarUi(appBarViewModel) }
43 | ) { innerPadding ->
44 | CheckUiContent(innerPadding, viewModel)
45 | }
46 | }
47 |
48 | @Composable
49 | fun CheckUiContent(innerPadding: PaddingValues, viewModel: CheckViewModel) {
50 | var showUrgencyDialog by remember { mutableStateOf(false) }
51 | val state = viewModel.uiState
52 | Column(
53 | modifier = Modifier
54 | .fillMaxSize()
55 | .padding(
56 | 16.dp,
57 | innerPadding.calculateTopPadding() + 16.dp,
58 | 16.dp,
59 | innerPadding.calculateBottomPadding() + 16.dp
60 | )
61 | .verticalScroll(rememberScrollState()),
62 | horizontalAlignment = Alignment.Start,
63 | verticalArrangement = Arrangement.spacedBy(16.dp)
64 | ) {
65 | Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
66 | Text(
67 | style = MaterialTheme.typography.labelMedium,
68 | text = "Endpoint"
69 | )
70 | SelectionContainer { Text(state.endpoint) }
71 | }
72 |
73 | Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
74 | Text(
75 | style = MaterialTheme.typography.labelMedium,
76 | text = "Auth"
77 | )
78 | SelectionContainer { Text(state.auth) }
79 | }
80 |
81 | Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
82 | Text(
83 | style = MaterialTheme.typography.labelMedium,
84 | text = "P256dh"
85 | )
86 | SelectionContainer { Text(state.p256dh) }
87 | }
88 |
89 | if (state.showVapid) {
90 | Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
91 | Text(
92 | style = MaterialTheme.typography.labelMedium,
93 | text = "VAPID"
94 | )
95 | SelectionContainer { Text(state.vapid) }
96 | }
97 | }
98 |
99 | Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
100 | Text(
101 | style = MaterialTheme.typography.labelMedium,
102 | text = "Test page"
103 | )
104 | Text(
105 | text = buildAnnotatedString {
106 | withLink(
107 | LinkAnnotation.Url(
108 | url = state.testPageUrl,
109 | styles = TextLinkStyles(
110 | style = SpanStyle(
111 | color = MaterialTheme.colorScheme.primary
112 | )
113 | )
114 | )
115 | ) {
116 | append(state.testPageUrl)
117 | }
118 | },
119 | maxLines = 1,
120 | overflow = TextOverflow.Ellipsis
121 | )
122 | }
123 |
124 | HorizontalDivider(thickness = 1.dp)
125 |
126 | state.error?.let {
127 | Text(it)
128 | }
129 |
130 | val buttonsList = mutableListOf<@Composable () -> Unit>(
131 | {
132 | Button(
133 | onClick = {
134 | Events.emit(Events.Type.Unregister)
135 | }
136 | ) {
137 | Text("Unregister")
138 | }
139 | },
140 | {
141 | Button(
142 | onClick = {
143 | Events.emit(Events.Type.SendNotification)
144 | }
145 | ) {
146 | Text("Send notification")
147 | }
148 | }
149 | )
150 | if (state.devMode) {
151 | buttonsList.addAll(
152 | listOf(
153 | {
154 | Button(
155 | onClick =
156 | {
157 | Events.emit(Events.Type.Reregister)
158 | }
159 | ) {
160 | Text("Reregister")
161 | }
162 | },
163 | {
164 | Button(
165 | enabled = state.hasForegroundService,
166 | onClick = {
167 | Events.emit(Events.Type.StopForegroundService)
168 | }
169 | ) {
170 | Text("Stop Foreground Service")
171 | }
172 | },
173 | {
174 | Button(
175 | onClick = {
176 | Events.emit(Events.Type.DeepLink)
177 | }
178 | ) {
179 | Text("Deep link")
180 | }
181 | },
182 | {
183 | Button(
184 | onClick = {
185 | Events.emit(Events.Type.ChangeDistributor)
186 | }
187 | ) {
188 | Text("Change distributor")
189 | }
190 | },
191 | {
192 | Button(
193 | enabled = !state.sendCleartext,
194 | onClick = {
195 | showUrgencyDialog = true
196 | }
197 | ) {
198 | Text("Set urgency")
199 | }
200 | },
201 | {
202 | Button(
203 | onClick = {
204 | Events.emit(Events.Type.UpdateVapidKey)
205 | }
206 | ) {
207 | Text("Update VAPID key")
208 | }
209 | },
210 | {
211 | Button(
212 | enabled = !state.sendCleartext,
213 | onClick = {
214 | Events.emit(Events.Type.TestTopic)
215 | }
216 | ) {
217 | Text("Test topic")
218 | }
219 | },
220 | {
221 | Button(
222 | enabled = !state.sendCleartext,
223 | onClick = {
224 | Events.emit(Events.Type.TestTTL)
225 | }
226 | ) {
227 | Text("Test TTL")
228 | }
229 | },
230 | {
231 | Button(
232 | onClick = {
233 | Events.emit(Events.Type.TestInBackground)
234 | }
235 | ) {
236 | Text("Test in background")
237 | }
238 | }
239 | )
240 | )
241 | }
242 |
243 | TwoColumns(buttonsList)
244 |
245 | if (showUrgencyDialog) {
246 | SetUrgencyDialog(
247 | urgency = state.urgency,
248 | onDismissRequest = { showUrgencyDialog = false },
249 | onConfirmation = {
250 | viewModel.setUrgency(it)
251 | showUrgencyDialog = false
252 | }
253 | )
254 | }
255 | }
256 | }
257 |
258 | @Composable
259 | fun TwoColumns(list: List<@Composable () -> Unit>) {
260 | list.chunked(2).forEach { item ->
261 | Row(
262 | modifier = Modifier.fillMaxWidth(),
263 | horizontalArrangement = Arrangement.SpaceEvenly
264 | ) {
265 | item[0]()
266 | if (item.size > 1) {
267 | item[1]()
268 | }
269 | }
270 | }
271 | }
272 |
273 | @Preview
274 | @Composable
275 | fun CheckUiPreview() {
276 | val endpoint = "https://my.endpoint.tld"
277 | val p256dh = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
278 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
279 | val auth = "Auth_random"
280 | val vapid = "vapid t=eyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
281 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
282 | val testUrl = genTestPageUrl(
283 | endpoint,
284 | p256dh,
285 | auth,
286 | vapid,
287 | true
288 | )
289 | CheckUiContent(
290 | PaddingValues(0.dp),
291 | CheckViewModel(
292 | CheckUiState(
293 | error = "error!",
294 | devMode = true,
295 | hasForegroundService = false,
296 | sendCleartext = true,
297 | endpoint = endpoint,
298 | auth = auth,
299 | p256dh = p256dh,
300 | showVapid = true,
301 | vapid = vapid,
302 | testPageUrl = testUrl,
303 | urgency = Urgency.NORMAL
304 | )
305 | )
306 | )
307 | }
308 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/ui/CheckUiState.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities.ui
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import org.unifiedpush.example.ApplicationServer
6 | import org.unifiedpush.example.Store
7 | import org.unifiedpush.example.TestService
8 | import org.unifiedpush.example.Urgency
9 | import org.unifiedpush.example.utils.TAG
10 | import org.unifiedpush.example.utils.genTestPageUrl
11 | import org.unifiedpush.example.utils.vapidImplementedForSdk
12 |
13 | data class CheckUiState(
14 | val error: String? = null,
15 | val devMode: Boolean = false,
16 | val hasForegroundService: Boolean = false,
17 | val sendCleartext: Boolean = false,
18 | val endpoint: String,
19 | val auth: String,
20 | val p256dh: String,
21 | val showVapid: Boolean,
22 | val vapid: String,
23 | val testPageUrl: String,
24 | val urgency: Urgency
25 | ) {
26 | companion object {
27 | fun error(): CheckUiState {
28 | return CheckUiState(
29 | null,
30 | false,
31 | endpoint = "Error",
32 | auth = "Error",
33 | p256dh = "Error",
34 | showVapid = false,
35 | vapid = "Error",
36 | testPageUrl = "Error",
37 | urgency = Urgency.NORMAL
38 | )
39 | }
40 |
41 | /**
42 | * @return `null` if the application doesn't have any endpoint
43 | */
44 | fun from(context: Context): CheckUiState? {
45 | val store = Store(context)
46 |
47 | /**
48 | * Auth secret, p256dh and VAPID should never be used when null,
49 | * we set a dummy value.
50 | */
51 | val endpoint = store.endpoint ?: return null
52 | val p256dh = store.serializedPubKey ?: "Error"
53 | val auth = store.b64authSecret ?: "Error"
54 | val showVapid = vapidImplementedForSdk() && store.devMode && store.devUseVapid
55 | val vapidHeader = if (vapidImplementedForSdk()) {
56 | ApplicationServer(context).getVapidHeader(fakeKeys = (store.devMode && store.devWrongVapidKeysTest))
57 | } else {
58 | "Error"
59 | }
60 | val testPageUrl = genTestPageUrl(
61 | endpoint,
62 | p256dh,
63 | auth,
64 | vapidHeader,
65 | showVapid
66 | )
67 | Log.d(TAG, "testPageUrl: $testPageUrl")
68 | return CheckUiState(
69 | devMode = store.devMode,
70 | hasForegroundService = TestService.isStarted(),
71 | sendCleartext = store.devCleartextTest,
72 | endpoint = endpoint,
73 | auth = auth,
74 | p256dh = p256dh,
75 | showVapid = showVapid,
76 | vapid = vapidHeader,
77 | testPageUrl = testPageUrl,
78 | urgency = store.urgency
79 | )
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/ui/MainUi.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities.ui
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material3.Button
11 | import androidx.compose.material3.Scaffold
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.runtime.setValue
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.res.stringResource
21 | import androidx.compose.ui.unit.dp
22 | import org.unifiedpush.example.R
23 | import org.unifiedpush.example.activities.AppBarViewModel
24 | import org.unifiedpush.example.activities.Events
25 |
26 | @Composable
27 | fun MainUi(viewModel: AppBarViewModel) {
28 | Scaffold(
29 | topBar = { AppBarUi(viewModel) }
30 | ) { innerPadding ->
31 | MainUiContent(innerPadding)
32 | }
33 | }
34 |
35 | @Composable
36 | fun MainUiContent(innerPadding: PaddingValues) {
37 | var showPermissionDialog by remember { mutableStateOf(true) }
38 |
39 | if (showPermissionDialog) {
40 | PermissionsUi {
41 | showPermissionDialog = false
42 | }
43 | }
44 | Column(
45 | modifier = Modifier
46 | .fillMaxSize()
47 | .padding(
48 | 16.dp,
49 | innerPadding.calculateTopPadding(),
50 | 16.dp,
51 | innerPadding.calculateBottomPadding()
52 | ),
53 | horizontalAlignment = Alignment.Start,
54 | verticalArrangement = Arrangement.spacedBy(16.dp)
55 | ) {
56 | Spacer(Modifier.height(16.dp))
57 | Text(stringResource(R.string.about))
58 | Button(
59 | onClick = {
60 | Events.emit(Events.Type.Register)
61 | }
62 | ) {
63 | Text("Register")
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/ui/PermissionsUi.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities.ui
2 |
3 | import android.os.Build
4 | import androidx.compose.material3.AlertDialog
5 | import androidx.compose.material3.Text
6 | import androidx.compose.material3.TextButton
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.res.stringResource
9 | import androidx.compose.ui.tooling.preview.Preview
10 | import com.google.accompanist.permissions.ExperimentalPermissionsApi
11 | import com.google.accompanist.permissions.isGranted
12 | import com.google.accompanist.permissions.rememberPermissionState
13 |
14 | @OptIn(ExperimentalPermissionsApi::class)
15 | @Preview
16 | @Composable
17 | fun PermissionsUi(onDone: () -> Unit = {}) {
18 | if (Build.VERSION.SDK_INT < 33) {
19 | onDone()
20 | return
21 | }
22 | val notificationsPermissionState =
23 | rememberPermissionState(
24 | android.Manifest.permission.POST_NOTIFICATIONS
25 | )
26 | if (!notificationsPermissionState.status.isGranted) {
27 | AlertDialog(
28 | title = {
29 | Text("Permissions")
30 | },
31 | text = {
32 | Text("This application requires notifications permission to work.")
33 | },
34 | onDismissRequest = {
35 | onDone()
36 | },
37 | confirmButton = {
38 | TextButton(
39 | onClick = {
40 | notificationsPermissionState.launchPermissionRequest()
41 | onDone()
42 | }
43 | ) {
44 | Text(stringResource(android.R.string.ok))
45 | }
46 | },
47 | dismissButton = {}
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/ui/SetUrgencyDialog.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities.ui
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.material3.AlertDialog
8 | import androidx.compose.material3.Checkbox
9 | import androidx.compose.material3.Text
10 | import androidx.compose.material3.TextButton
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.tooling.preview.Preview
16 | import androidx.compose.ui.unit.dp
17 | import org.unifiedpush.example.Urgency
18 |
19 | @Preview
20 | @Composable
21 | fun SetUrgencyDialog(urgency: Urgency = Urgency.NORMAL, onDismissRequest: () -> Unit = {}, onConfirmation: (Urgency) -> Unit = {}) {
22 | AlertDialog(
23 | title = {
24 | Text("Urgency")
25 | },
26 | text = {
27 | Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
28 | Urgency.entries.forEach { entry ->
29 | Row(verticalAlignment = Alignment.CenterVertically) {
30 | TextButton(
31 | onClick = { onConfirmation(entry) }
32 | ) {
33 | Text(
34 | text = entry.value
35 | )
36 | }
37 | Spacer(Modifier.weight(1f))
38 | Checkbox(
39 | entry == urgency,
40 | onCheckedChange = { onConfirmation(entry) }
41 | )
42 | }
43 | }
44 | }
45 | },
46 | onDismissRequest = {
47 | onDismissRequest()
48 | },
49 | confirmButton = { },
50 | dismissButton = {
51 | TextButton(
52 | onClick = {
53 | onDismissRequest()
54 | }
55 | ) {
56 | Text(stringResource(android.R.string.cancel))
57 | }
58 | }
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val primaryLight = Color(0xFF6A538C)
6 | val onPrimaryLight = Color(0xFFFFFFFF)
7 | val primaryContainerLight = Color(0xFFEDDCFF)
8 | val onPrimaryContainerLight = Color(0xFF250E44)
9 | val secondaryLight = Color(0xFF645A70)
10 | val onSecondaryLight = Color(0xFFFFFFFF)
11 | val secondaryContainerLight = Color(0xFFEBDDF7)
12 | val onSecondaryContainerLight = Color(0xFF20182A)
13 | val tertiaryLight = Color(0xFF7F525B)
14 | val onTertiaryLight = Color(0xFFFFFFFF)
15 | val tertiaryContainerLight = Color(0xFFFFD9DF)
16 | val onTertiaryContainerLight = Color(0xFF321019)
17 | val errorLight = Color(0xFFBA1A1A)
18 | val onErrorLight = Color(0xFFFFFFFF)
19 | val errorContainerLight = Color(0xFFFFDAD6)
20 | val onErrorContainerLight = Color(0xFF410002)
21 | val backgroundLight = Color(0xFFFEF7FF)
22 | val onBackgroundLight = Color(0xFF1D1A20)
23 | val surfaceLight = Color(0xFFFEF7FF)
24 | val onSurfaceLight = Color(0xFF1D1A20)
25 | val surfaceVariantLight = Color(0xFFE8E0EB)
26 | val onSurfaceVariantLight = Color(0xFF4A454E)
27 | val outlineLight = Color(0xFF7B757F)
28 | val outlineVariantLight = Color(0xFFCBC4CF)
29 | val scrimLight = Color(0xFF000000)
30 | val inverseSurfaceLight = Color(0xFF322F35)
31 | val inverseOnSurfaceLight = Color(0xFFF6EEF7)
32 | val inversePrimaryLight = Color(0xFFD6BBFB)
33 | val surfaceDimLight = Color(0xFFDFD8E0)
34 | val surfaceBrightLight = Color(0xFFFEF7FF)
35 | val surfaceContainerLowestLight = Color(0xFFFFFFFF)
36 | val surfaceContainerLowLight = Color(0xFFF9F1F9)
37 | val surfaceContainerLight = Color(0xFFF3ECF4)
38 | val surfaceContainerHighLight = Color(0xFFEDE6EE)
39 | val surfaceContainerHighestLight = Color(0xFFE7E0E8)
40 |
41 | val primaryLightMediumContrast = Color(0xFF4E386F)
42 | val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
43 | val primaryContainerLightMediumContrast = Color(0xFF816AA4)
44 | val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
45 | val secondaryLightMediumContrast = Color(0xFF483F53)
46 | val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
47 | val secondaryContainerLightMediumContrast = Color(0xFF7B7087)
48 | val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
49 | val tertiaryLightMediumContrast = Color(0xFF603740)
50 | val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
51 | val tertiaryContainerLightMediumContrast = Color(0xFF986771)
52 | val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
53 | val errorLightMediumContrast = Color(0xFF8C0009)
54 | val onErrorLightMediumContrast = Color(0xFFFFFFFF)
55 | val errorContainerLightMediumContrast = Color(0xFFDA342E)
56 | val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
57 | val backgroundLightMediumContrast = Color(0xFFFEF7FF)
58 | val onBackgroundLightMediumContrast = Color(0xFF1D1A20)
59 | val surfaceLightMediumContrast = Color(0xFFFEF7FF)
60 | val onSurfaceLightMediumContrast = Color(0xFF1D1A20)
61 | val surfaceVariantLightMediumContrast = Color(0xFFE8E0EB)
62 | val onSurfaceVariantLightMediumContrast = Color(0xFF46414A)
63 | val outlineLightMediumContrast = Color(0xFF625D67)
64 | val outlineVariantLightMediumContrast = Color(0xFF7E7982)
65 | val scrimLightMediumContrast = Color(0xFF000000)
66 | val inverseSurfaceLightMediumContrast = Color(0xFF322F35)
67 | val inverseOnSurfaceLightMediumContrast = Color(0xFFF6EEF7)
68 | val inversePrimaryLightMediumContrast = Color(0xFFD6BBFB)
69 | val surfaceDimLightMediumContrast = Color(0xFFDFD8E0)
70 | val surfaceBrightLightMediumContrast = Color(0xFFFEF7FF)
71 | val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
72 | val surfaceContainerLowLightMediumContrast = Color(0xFFF9F1F9)
73 | val surfaceContainerLightMediumContrast = Color(0xFFF3ECF4)
74 | val surfaceContainerHighLightMediumContrast = Color(0xFFEDE6EE)
75 | val surfaceContainerHighestLightMediumContrast = Color(0xFFE7E0E8)
76 |
77 | val primaryLightHighContrast = Color(0xFF2C164B)
78 | val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
79 | val primaryContainerLightHighContrast = Color(0xFF4E386F)
80 | val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
81 | val secondaryLightHighContrast = Color(0xFF261F31)
82 | val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
83 | val secondaryContainerLightHighContrast = Color(0xFF483F53)
84 | val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
85 | val tertiaryLightHighContrast = Color(0xFF3A1720)
86 | val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
87 | val tertiaryContainerLightHighContrast = Color(0xFF603740)
88 | val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
89 | val errorLightHighContrast = Color(0xFF4E0002)
90 | val onErrorLightHighContrast = Color(0xFFFFFFFF)
91 | val errorContainerLightHighContrast = Color(0xFF8C0009)
92 | val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
93 | val backgroundLightHighContrast = Color(0xFFFEF7FF)
94 | val onBackgroundLightHighContrast = Color(0xFF1D1A20)
95 | val surfaceLightHighContrast = Color(0xFFFEF7FF)
96 | val onSurfaceLightHighContrast = Color(0xFF000000)
97 | val surfaceVariantLightHighContrast = Color(0xFFE8E0EB)
98 | val onSurfaceVariantLightHighContrast = Color(0xFF26222A)
99 | val outlineLightHighContrast = Color(0xFF46414A)
100 | val outlineVariantLightHighContrast = Color(0xFF46414A)
101 | val scrimLightHighContrast = Color(0xFF000000)
102 | val inverseSurfaceLightHighContrast = Color(0xFF322F35)
103 | val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
104 | val inversePrimaryLightHighContrast = Color(0xFFF4E7FF)
105 | val surfaceDimLightHighContrast = Color(0xFFDFD8E0)
106 | val surfaceBrightLightHighContrast = Color(0xFFFEF7FF)
107 | val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
108 | val surfaceContainerLowLightHighContrast = Color(0xFFF9F1F9)
109 | val surfaceContainerLightHighContrast = Color(0xFFF3ECF4)
110 | val surfaceContainerHighLightHighContrast = Color(0xFFEDE6EE)
111 | val surfaceContainerHighestLightHighContrast = Color(0xFFE7E0E8)
112 |
113 | val primaryDark = Color(0xFFD6BBFB)
114 | val onPrimaryDark = Color(0xFF3B255B)
115 | val primaryContainerDark = Color(0xFF523C73)
116 | val onPrimaryContainerDark = Color(0xFFEDDCFF)
117 | val secondaryDark = Color(0xFFCEC2DA)
118 | val onSecondaryDark = Color(0xFF352D40)
119 | val secondaryContainerDark = Color(0xFF4C4357)
120 | val onSecondaryContainerDark = Color(0xFFEBDDF7)
121 | val tertiaryDark = Color(0xFFF1B7C2)
122 | val onTertiaryDark = Color(0xFF4B252E)
123 | val tertiaryContainerDark = Color(0xFF653B44)
124 | val onTertiaryContainerDark = Color(0xFFFFD9DF)
125 | val errorDark = Color(0xFFFFB4AB)
126 | val onErrorDark = Color(0xFF690005)
127 | val errorContainerDark = Color(0xFF93000A)
128 | val onErrorContainerDark = Color(0xFFFFDAD6)
129 | val backgroundDark = Color(0xFF151218)
130 | val onBackgroundDark = Color(0xFFE7E0E8)
131 | val surfaceDark = Color(0xFF151218)
132 | val onSurfaceDark = Color(0xFFE7E0E8)
133 | val surfaceVariantDark = Color(0xFF4A454E)
134 | val onSurfaceVariantDark = Color(0xFFCBC4CF)
135 | val outlineDark = Color(0xFF958E99)
136 | val outlineVariantDark = Color(0xFF4A454E)
137 | val scrimDark = Color(0xFF000000)
138 | val inverseSurfaceDark = Color(0xFFE7E0E8)
139 | val inverseOnSurfaceDark = Color(0xFF322F35)
140 | val inversePrimaryDark = Color(0xFF6A538C)
141 | val surfaceDimDark = Color(0xFF151218)
142 | val surfaceBrightDark = Color(0xFF3B383E)
143 | val surfaceContainerLowestDark = Color(0xFF100D12)
144 | val surfaceContainerLowDark = Color(0xFF1D1A20)
145 | val surfaceContainerDark = Color(0xFF211E24)
146 | val surfaceContainerHighDark = Color(0xFF2C292F)
147 | val surfaceContainerHighestDark = Color(0xFF37333A)
148 |
149 | val primaryDarkMediumContrast = Color(0xFFDABFFF)
150 | val onPrimaryDarkMediumContrast = Color(0xFF1F073F)
151 | val primaryContainerDarkMediumContrast = Color(0xFF9E85C2)
152 | val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
153 | val secondaryDarkMediumContrast = Color(0xFFD3C6DF)
154 | val onSecondaryDarkMediumContrast = Color(0xFF1A1325)
155 | val secondaryContainerDarkMediumContrast = Color(0xFF978CA3)
156 | val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
157 | val tertiaryDarkMediumContrast = Color(0xFFF6BBC6)
158 | val onTertiaryDarkMediumContrast = Color(0xFF2B0B14)
159 | val tertiaryContainerDarkMediumContrast = Color(0xFFB7838D)
160 | val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
161 | val errorDarkMediumContrast = Color(0xFFFFBAB1)
162 | val onErrorDarkMediumContrast = Color(0xFF370001)
163 | val errorContainerDarkMediumContrast = Color(0xFFFF5449)
164 | val onErrorContainerDarkMediumContrast = Color(0xFF000000)
165 | val backgroundDarkMediumContrast = Color(0xFF151218)
166 | val onBackgroundDarkMediumContrast = Color(0xFFE7E0E8)
167 | val surfaceDarkMediumContrast = Color(0xFF151218)
168 | val onSurfaceDarkMediumContrast = Color(0xFFFFF9FD)
169 | val surfaceVariantDarkMediumContrast = Color(0xFF4A454E)
170 | val onSurfaceVariantDarkMediumContrast = Color(0xFFD0C8D3)
171 | val outlineDarkMediumContrast = Color(0xFFA7A0AB)
172 | val outlineVariantDarkMediumContrast = Color(0xFF87818B)
173 | val scrimDarkMediumContrast = Color(0xFF000000)
174 | val inverseSurfaceDarkMediumContrast = Color(0xFFE7E0E8)
175 | val inverseOnSurfaceDarkMediumContrast = Color(0xFF2C292F)
176 | val inversePrimaryDarkMediumContrast = Color(0xFF533D74)
177 | val surfaceDimDarkMediumContrast = Color(0xFF151218)
178 | val surfaceBrightDarkMediumContrast = Color(0xFF3B383E)
179 | val surfaceContainerLowestDarkMediumContrast = Color(0xFF100D12)
180 | val surfaceContainerLowDarkMediumContrast = Color(0xFF1D1A20)
181 | val surfaceContainerDarkMediumContrast = Color(0xFF211E24)
182 | val surfaceContainerHighDarkMediumContrast = Color(0xFF2C292F)
183 | val surfaceContainerHighestDarkMediumContrast = Color(0xFF37333A)
184 |
185 | val primaryDarkHighContrast = Color(0xFFFFF9FD)
186 | val onPrimaryDarkHighContrast = Color(0xFF000000)
187 | val primaryContainerDarkHighContrast = Color(0xFFDABFFF)
188 | val onPrimaryContainerDarkHighContrast = Color(0xFF000000)
189 | val secondaryDarkHighContrast = Color(0xFFFFF9FD)
190 | val onSecondaryDarkHighContrast = Color(0xFF000000)
191 | val secondaryContainerDarkHighContrast = Color(0xFFD3C6DF)
192 | val onSecondaryContainerDarkHighContrast = Color(0xFF000000)
193 | val tertiaryDarkHighContrast = Color(0xFFFFF9F9)
194 | val onTertiaryDarkHighContrast = Color(0xFF000000)
195 | val tertiaryContainerDarkHighContrast = Color(0xFFF6BBC6)
196 | val onTertiaryContainerDarkHighContrast = Color(0xFF000000)
197 | val errorDarkHighContrast = Color(0xFFFFF9F9)
198 | val onErrorDarkHighContrast = Color(0xFF000000)
199 | val errorContainerDarkHighContrast = Color(0xFFFFBAB1)
200 | val onErrorContainerDarkHighContrast = Color(0xFF000000)
201 | val backgroundDarkHighContrast = Color(0xFF151218)
202 | val onBackgroundDarkHighContrast = Color(0xFFE7E0E8)
203 | val surfaceDarkHighContrast = Color(0xFF151218)
204 | val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
205 | val surfaceVariantDarkHighContrast = Color(0xFF4A454E)
206 | val onSurfaceVariantDarkHighContrast = Color(0xFFFFF9FD)
207 | val outlineDarkHighContrast = Color(0xFFD0C8D3)
208 | val outlineVariantDarkHighContrast = Color(0xFFD0C8D3)
209 | val scrimDarkHighContrast = Color(0xFF000000)
210 | val inverseSurfaceDarkHighContrast = Color(0xFFE7E0E8)
211 | val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
212 | val inversePrimaryDarkHighContrast = Color(0xFF341E54)
213 | val surfaceDimDarkHighContrast = Color(0xFF151218)
214 | val surfaceBrightDarkHighContrast = Color(0xFF3B383E)
215 | val surfaceContainerLowestDarkHighContrast = Color(0xFF100D12)
216 | val surfaceContainerLowDarkHighContrast = Color(0xFF1D1A20)
217 | val surfaceContainerDarkHighContrast = Color(0xFF211E24)
218 | val surfaceContainerHighDarkHighContrast = Color(0xFF2C292F)
219 | val surfaceContainerHighestDarkHighContrast = Color(0xFF37333A)
220 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.Immutable
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.platform.LocalContext
14 |
15 | private val lightScheme = lightColorScheme(
16 | primary = primaryLight,
17 | onPrimary = onPrimaryLight,
18 | primaryContainer = primaryContainerLight,
19 | onPrimaryContainer = onPrimaryContainerLight,
20 | secondary = secondaryLight,
21 | onSecondary = onSecondaryLight,
22 | secondaryContainer = secondaryContainerLight,
23 | onSecondaryContainer = onSecondaryContainerLight,
24 | tertiary = tertiaryLight,
25 | onTertiary = onTertiaryLight,
26 | tertiaryContainer = tertiaryContainerLight,
27 | onTertiaryContainer = onTertiaryContainerLight,
28 | error = errorLight,
29 | onError = onErrorLight,
30 | errorContainer = errorContainerLight,
31 | onErrorContainer = onErrorContainerLight,
32 | background = backgroundLight,
33 | onBackground = onBackgroundLight,
34 | surface = surfaceLight,
35 | onSurface = onSurfaceLight,
36 | surfaceVariant = surfaceVariantLight,
37 | onSurfaceVariant = onSurfaceVariantLight,
38 | outline = outlineLight,
39 | outlineVariant = outlineVariantLight,
40 | scrim = scrimLight,
41 | inverseSurface = inverseSurfaceLight,
42 | inverseOnSurface = inverseOnSurfaceLight,
43 | inversePrimary = inversePrimaryLight,
44 | surfaceDim = surfaceDimLight,
45 | surfaceBright = surfaceBrightLight,
46 | surfaceContainerLowest = surfaceContainerLowestLight,
47 | surfaceContainerLow = surfaceContainerLowLight,
48 | surfaceContainer = surfaceContainerLight,
49 | surfaceContainerHigh = surfaceContainerHighLight,
50 | surfaceContainerHighest = surfaceContainerHighestLight
51 | )
52 |
53 | private val darkScheme = darkColorScheme(
54 | primary = primaryDark,
55 | onPrimary = onPrimaryDark,
56 | primaryContainer = primaryContainerDark,
57 | onPrimaryContainer = onPrimaryContainerDark,
58 | secondary = secondaryDark,
59 | onSecondary = onSecondaryDark,
60 | secondaryContainer = secondaryContainerDark,
61 | onSecondaryContainer = onSecondaryContainerDark,
62 | tertiary = tertiaryDark,
63 | onTertiary = onTertiaryDark,
64 | tertiaryContainer = tertiaryContainerDark,
65 | onTertiaryContainer = onTertiaryContainerDark,
66 | error = errorDark,
67 | onError = onErrorDark,
68 | errorContainer = errorContainerDark,
69 | onErrorContainer = onErrorContainerDark,
70 | background = backgroundDark,
71 | onBackground = onBackgroundDark,
72 | surface = surfaceDark,
73 | onSurface = onSurfaceDark,
74 | surfaceVariant = surfaceVariantDark,
75 | onSurfaceVariant = onSurfaceVariantDark,
76 | outline = outlineDark,
77 | outlineVariant = outlineVariantDark,
78 | scrim = scrimDark,
79 | inverseSurface = inverseSurfaceDark,
80 | inverseOnSurface = inverseOnSurfaceDark,
81 | inversePrimary = inversePrimaryDark,
82 | surfaceDim = surfaceDimDark,
83 | surfaceBright = surfaceBrightDark,
84 | surfaceContainerLowest = surfaceContainerLowestDark,
85 | surfaceContainerLow = surfaceContainerLowDark,
86 | surfaceContainer = surfaceContainerDark,
87 | surfaceContainerHigh = surfaceContainerHighDark,
88 | surfaceContainerHighest = surfaceContainerHighestDark
89 | )
90 |
91 | private val mediumContrastLightColorScheme = lightColorScheme(
92 | primary = primaryLightMediumContrast,
93 | onPrimary = onPrimaryLightMediumContrast,
94 | primaryContainer = primaryContainerLightMediumContrast,
95 | onPrimaryContainer = onPrimaryContainerLightMediumContrast,
96 | secondary = secondaryLightMediumContrast,
97 | onSecondary = onSecondaryLightMediumContrast,
98 | secondaryContainer = secondaryContainerLightMediumContrast,
99 | onSecondaryContainer = onSecondaryContainerLightMediumContrast,
100 | tertiary = tertiaryLightMediumContrast,
101 | onTertiary = onTertiaryLightMediumContrast,
102 | tertiaryContainer = tertiaryContainerLightMediumContrast,
103 | onTertiaryContainer = onTertiaryContainerLightMediumContrast,
104 | error = errorLightMediumContrast,
105 | onError = onErrorLightMediumContrast,
106 | errorContainer = errorContainerLightMediumContrast,
107 | onErrorContainer = onErrorContainerLightMediumContrast,
108 | background = backgroundLightMediumContrast,
109 | onBackground = onBackgroundLightMediumContrast,
110 | surface = surfaceLightMediumContrast,
111 | onSurface = onSurfaceLightMediumContrast,
112 | surfaceVariant = surfaceVariantLightMediumContrast,
113 | onSurfaceVariant = onSurfaceVariantLightMediumContrast,
114 | outline = outlineLightMediumContrast,
115 | outlineVariant = outlineVariantLightMediumContrast,
116 | scrim = scrimLightMediumContrast,
117 | inverseSurface = inverseSurfaceLightMediumContrast,
118 | inverseOnSurface = inverseOnSurfaceLightMediumContrast,
119 | inversePrimary = inversePrimaryLightMediumContrast,
120 | surfaceDim = surfaceDimLightMediumContrast,
121 | surfaceBright = surfaceBrightLightMediumContrast,
122 | surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
123 | surfaceContainerLow = surfaceContainerLowLightMediumContrast,
124 | surfaceContainer = surfaceContainerLightMediumContrast,
125 | surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
126 | surfaceContainerHighest = surfaceContainerHighestLightMediumContrast
127 | )
128 |
129 | private val highContrastLightColorScheme = lightColorScheme(
130 | primary = primaryLightHighContrast,
131 | onPrimary = onPrimaryLightHighContrast,
132 | primaryContainer = primaryContainerLightHighContrast,
133 | onPrimaryContainer = onPrimaryContainerLightHighContrast,
134 | secondary = secondaryLightHighContrast,
135 | onSecondary = onSecondaryLightHighContrast,
136 | secondaryContainer = secondaryContainerLightHighContrast,
137 | onSecondaryContainer = onSecondaryContainerLightHighContrast,
138 | tertiary = tertiaryLightHighContrast,
139 | onTertiary = onTertiaryLightHighContrast,
140 | tertiaryContainer = tertiaryContainerLightHighContrast,
141 | onTertiaryContainer = onTertiaryContainerLightHighContrast,
142 | error = errorLightHighContrast,
143 | onError = onErrorLightHighContrast,
144 | errorContainer = errorContainerLightHighContrast,
145 | onErrorContainer = onErrorContainerLightHighContrast,
146 | background = backgroundLightHighContrast,
147 | onBackground = onBackgroundLightHighContrast,
148 | surface = surfaceLightHighContrast,
149 | onSurface = onSurfaceLightHighContrast,
150 | surfaceVariant = surfaceVariantLightHighContrast,
151 | onSurfaceVariant = onSurfaceVariantLightHighContrast,
152 | outline = outlineLightHighContrast,
153 | outlineVariant = outlineVariantLightHighContrast,
154 | scrim = scrimLightHighContrast,
155 | inverseSurface = inverseSurfaceLightHighContrast,
156 | inverseOnSurface = inverseOnSurfaceLightHighContrast,
157 | inversePrimary = inversePrimaryLightHighContrast,
158 | surfaceDim = surfaceDimLightHighContrast,
159 | surfaceBright = surfaceBrightLightHighContrast,
160 | surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
161 | surfaceContainerLow = surfaceContainerLowLightHighContrast,
162 | surfaceContainer = surfaceContainerLightHighContrast,
163 | surfaceContainerHigh = surfaceContainerHighLightHighContrast,
164 | surfaceContainerHighest = surfaceContainerHighestLightHighContrast
165 | )
166 |
167 | private val mediumContrastDarkColorScheme = darkColorScheme(
168 | primary = primaryDarkMediumContrast,
169 | onPrimary = onPrimaryDarkMediumContrast,
170 | primaryContainer = primaryContainerDarkMediumContrast,
171 | onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
172 | secondary = secondaryDarkMediumContrast,
173 | onSecondary = onSecondaryDarkMediumContrast,
174 | secondaryContainer = secondaryContainerDarkMediumContrast,
175 | onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
176 | tertiary = tertiaryDarkMediumContrast,
177 | onTertiary = onTertiaryDarkMediumContrast,
178 | tertiaryContainer = tertiaryContainerDarkMediumContrast,
179 | onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
180 | error = errorDarkMediumContrast,
181 | onError = onErrorDarkMediumContrast,
182 | errorContainer = errorContainerDarkMediumContrast,
183 | onErrorContainer = onErrorContainerDarkMediumContrast,
184 | background = backgroundDarkMediumContrast,
185 | onBackground = onBackgroundDarkMediumContrast,
186 | surface = surfaceDarkMediumContrast,
187 | onSurface = onSurfaceDarkMediumContrast,
188 | surfaceVariant = surfaceVariantDarkMediumContrast,
189 | onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
190 | outline = outlineDarkMediumContrast,
191 | outlineVariant = outlineVariantDarkMediumContrast,
192 | scrim = scrimDarkMediumContrast,
193 | inverseSurface = inverseSurfaceDarkMediumContrast,
194 | inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
195 | inversePrimary = inversePrimaryDarkMediumContrast,
196 | surfaceDim = surfaceDimDarkMediumContrast,
197 | surfaceBright = surfaceBrightDarkMediumContrast,
198 | surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
199 | surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
200 | surfaceContainer = surfaceContainerDarkMediumContrast,
201 | surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
202 | surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast
203 | )
204 |
205 | private val highContrastDarkColorScheme = darkColorScheme(
206 | primary = primaryDarkHighContrast,
207 | onPrimary = onPrimaryDarkHighContrast,
208 | primaryContainer = primaryContainerDarkHighContrast,
209 | onPrimaryContainer = onPrimaryContainerDarkHighContrast,
210 | secondary = secondaryDarkHighContrast,
211 | onSecondary = onSecondaryDarkHighContrast,
212 | secondaryContainer = secondaryContainerDarkHighContrast,
213 | onSecondaryContainer = onSecondaryContainerDarkHighContrast,
214 | tertiary = tertiaryDarkHighContrast,
215 | onTertiary = onTertiaryDarkHighContrast,
216 | tertiaryContainer = tertiaryContainerDarkHighContrast,
217 | onTertiaryContainer = onTertiaryContainerDarkHighContrast,
218 | error = errorDarkHighContrast,
219 | onError = onErrorDarkHighContrast,
220 | errorContainer = errorContainerDarkHighContrast,
221 | onErrorContainer = onErrorContainerDarkHighContrast,
222 | background = backgroundDarkHighContrast,
223 | onBackground = onBackgroundDarkHighContrast,
224 | surface = surfaceDarkHighContrast,
225 | onSurface = onSurfaceDarkHighContrast,
226 | surfaceVariant = surfaceVariantDarkHighContrast,
227 | onSurfaceVariant = onSurfaceVariantDarkHighContrast,
228 | outline = outlineDarkHighContrast,
229 | outlineVariant = outlineVariantDarkHighContrast,
230 | scrim = scrimDarkHighContrast,
231 | inverseSurface = inverseSurfaceDarkHighContrast,
232 | inverseOnSurface = inverseOnSurfaceDarkHighContrast,
233 | inversePrimary = inversePrimaryDarkHighContrast,
234 | surfaceDim = surfaceDimDarkHighContrast,
235 | surfaceBright = surfaceBrightDarkHighContrast,
236 | surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
237 | surfaceContainerLow = surfaceContainerLowDarkHighContrast,
238 | surfaceContainer = surfaceContainerDarkHighContrast,
239 | surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
240 | surfaceContainerHighest = surfaceContainerHighestDarkHighContrast
241 | )
242 |
243 | @Immutable
244 | data class ColorFamily(
245 | val color: Color,
246 | val onColor: Color,
247 | val colorContainer: Color,
248 | val onColorContainer: Color
249 | )
250 |
251 | val unspecified_scheme = ColorFamily(
252 | Color.Unspecified,
253 | Color.Unspecified,
254 | Color.Unspecified,
255 | Color.Unspecified
256 | )
257 |
258 | @Composable
259 | fun AppTheme(
260 | darkTheme: Boolean = isSystemInDarkTheme(),
261 | // Dynamic color is available on Android 12+
262 | dynamicColor: Boolean = false,
263 | content:
264 | @Composable()
265 | () -> Unit
266 | ) {
267 | val colorScheme = when {
268 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
269 | val context = LocalContext.current
270 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
271 | }
272 |
273 | darkTheme -> darkScheme
274 | else -> lightScheme
275 | }
276 |
277 | MaterialTheme(
278 | colorScheme = colorScheme,
279 | typography = AppTypography,
280 | content = content
281 | )
282 | }
283 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/activities/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.activities.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 |
5 | val AppTypography = Typography()
6 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/utils/DelayedRequestWorker.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.utils
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.work.ExistingWorkPolicy
6 | import androidx.work.OneTimeWorkRequest
7 | import androidx.work.WorkManager
8 | import androidx.work.Worker
9 | import androidx.work.WorkerParameters
10 | import java.util.concurrent.TimeUnit
11 | import org.unifiedpush.example.ApplicationServer
12 |
13 | class DelayedRequestWorker(context: Context, workerParams: WorkerParameters) :
14 | Worker(
15 | context,
16 | workerParams
17 | ) {
18 | override fun doWork(): Result {
19 | val tag = DelayedRequestWorker::class.java.simpleName
20 | ApplicationServer(applicationContext).sendNotification { e ->
21 | e?.let {
22 | Log.d(tag, "An error occurred. $e")
23 | } ?: Log.d(tag, "Notification sent.")
24 | }
25 | return Result.success()
26 | }
27 |
28 | companion object {
29 | fun enqueue(context: Context, delayMs: Long) {
30 | val worker =
31 | OneTimeWorkRequest.Builder(DelayedRequestWorker::class.java).apply {
32 | setInitialDelay(delayMs, TimeUnit.MILLISECONDS)
33 | }
34 | WorkManager.getInstance(context)
35 | .beginUniqueWork("BackgroundTest", ExistingWorkPolicy.REPLACE, worker.build())
36 | .enqueue()
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/utils/MessageParser.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.utils
2 |
3 | import java.net.URLDecoder
4 |
5 | private const val COULD_NOT_DECRYPT = "Could not decrypt content."
6 |
7 | /**
8 | * Decode message as a `application/x-www-form-urlencoded` message.
9 | *
10 | * If the message doesn't contain "title=" and "message=", the full body is considered as the message.
11 | * If the message contains unknown characters, the message is [COULD_NOT_DECRYPT].
12 | *
13 | * @return a map of key => value.
14 | * "title=myTitle&message=myContent" will return a Map {"title"=>"myTitle, "message"=>"myContent"}
15 | */
16 | internal fun decodeMessage(message: String): Map {
17 | val params =
18 | try {
19 | val dict = message.split("&")
20 | dict.associate {
21 | try {
22 | URLDecoder.decode(it.split("=")[0], "UTF-8") to
23 | URLDecoder.decode(it.split("=")[1], "UTF-8")
24 | } catch (e: Exception) {
25 | "" to ""
26 | }
27 | }
28 | } catch (e: Exception) {
29 | notDecodedMap(message)
30 | }
31 | if (params.keys.contains("message") && params.keys.contains("title")) {
32 | return params
33 | }
34 | return notDecodedMap(message)
35 | }
36 |
37 | private fun notDecodedMap(message: String): Map {
38 | return if (message.all { it.isDefined() && !it.isISOControl() }) {
39 | mapOf(
40 | "message" to message
41 | )
42 | } else {
43 | mapOf(
44 | "message" to COULD_NOT_DECRYPT
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/utils/Notifier.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.utils
2 |
3 | import android.app.Notification
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.content.Context
7 | import android.os.Build
8 | import androidx.annotation.RequiresApi
9 | import java.util.concurrent.ThreadLocalRandom
10 | import org.unifiedpush.example.R
11 |
12 | class Notifier(var context: Context) {
13 | private val channelId = context.packageName
14 | private val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
15 |
16 | init {
17 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
18 | createNotificationChannel()
19 | }
20 | }
21 |
22 | fun showNotification(title: String, text: String, priority: Int) {
23 | val notificationBuilder =
24 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
25 | Notification.Builder(context, channelId)
26 | } else {
27 | Notification.Builder(context)
28 | }
29 |
30 | val notification =
31 | notificationBuilder.setSmallIcon(R.drawable.ic_launcher_notification) // the status icon
32 | .setTicker(text) // the status text
33 | .setWhen(System.currentTimeMillis()) // the time stamp
34 | .setContentTitle(title) // the label of the entry
35 | .setContentText(text) // the contents of the entry
36 | .setPriority(priority)
37 | .build()
38 |
39 | val notificationId =
40 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
41 | ThreadLocalRandom.current().nextInt()
42 | } else {
43 | 13737
44 | }
45 | nm.notify(notificationId, notification)
46 | }
47 |
48 | @RequiresApi(26)
49 | private fun createNotificationChannel() {
50 | val name = context.packageName
51 | val descriptionText = "Test notifications"
52 | val importance = NotificationManager.IMPORTANCE_DEFAULT
53 | val channel =
54 | NotificationChannel(channelId, name, importance).apply {
55 | description = descriptionText
56 | }
57 | nm.createNotificationChannel(channel)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/utils/RawRequest.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.utils
2 |
3 | import androidx.annotation.GuardedBy
4 | import com.android.volley.NetworkResponse
5 | import com.android.volley.Request
6 | import com.android.volley.Response
7 | import com.android.volley.toolbox.HttpHeaderParser
8 |
9 | /**
10 | * Creates a new request with the given method.
11 | *
12 | * @param method the request method to use
13 | * @param url URL to fetch the string at
14 | * @param mListener Listener to receive the NetworkResponse response
15 | * @param errorListener Error listener, or null to ignore errors
16 | */
17 | open class RawRequest(
18 | method: Int,
19 | url: String?,
20 | @field:GuardedBy("mLock") private var mListener: Response.Listener?,
21 | errorListener: Response.ErrorListener?
22 | ) :
23 | Request(method, url, errorListener) {
24 | /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */
25 | private val mLock = Any()
26 |
27 | /**
28 | * Creates a new GET request.
29 | *
30 | * @param url URL to fetch the string at
31 | * @param listener Listener to receive the NetworkResponse response
32 | * @param errorListener Error listener, or null to ignore errors
33 | */
34 | constructor(
35 | url: String?,
36 | listener: Response.Listener?,
37 | errorListener: Response.ErrorListener?
38 | ) : this(
39 | Method.GET,
40 | url,
41 | listener,
42 | errorListener
43 | )
44 |
45 | override fun cancel() {
46 | super.cancel()
47 | synchronized(mLock) {
48 | mListener = null
49 | }
50 | }
51 |
52 | override fun deliverResponse(response: NetworkResponse) {
53 | var listener: Response.Listener?
54 | synchronized(mLock) {
55 | listener = mListener
56 | }
57 | if (listener != null) {
58 | listener!!.onResponse(response)
59 | }
60 | }
61 |
62 | override fun parseNetworkResponse(response: NetworkResponse): Response {
63 | return Response.success(response, HttpHeaderParser.parseCacheHeaders(response))
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/utils/RegistrationDialogs.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.utils
2 |
3 | import android.content.Context
4 | import org.unifiedpush.android.connector.UnifiedPush
5 | import org.unifiedpush.android.connector.ui.SelectDistributorDialogsBuilder
6 | import org.unifiedpush.android.connector.ui.UnifiedPushFunctions
7 | import org.unifiedpush.example.Store
8 |
9 | class RegistrationDialogs(
10 | context: Context,
11 | override var mayUseCurrent: Boolean,
12 | override var mayUseDefault: Boolean
13 | ) : SelectDistributorDialogsBuilder(
14 | context,
15 | object : UnifiedPushFunctions {
16 | override fun tryUseDefaultDistributor(callback: (Boolean) -> Unit) {
17 | UnifiedPush.tryUseDefaultDistributor(context, callback)
18 | }
19 |
20 | override fun getAckDistributor(): String? {
21 | return UnifiedPush.getAckDistributor(context)
22 | }
23 |
24 | override fun getDistributors(): List {
25 | return UnifiedPush.getDistributors(context)
26 | }
27 |
28 | override fun register(instance: String) {
29 | val store = Store(context)
30 | val vapid = if (store.devMode && store.devUseVapid) store.vapidPubKey else null
31 | UnifiedPush.register(context, instance, vapid = vapid)
32 | }
33 |
34 | override fun saveDistributor(distributor: String) {
35 | UnifiedPush.saveDistributor(context, distributor)
36 | }
37 | }
38 | )
39 | /* {
40 | /**
41 | * This is an example to ignore noDistributorFound
42 | */
43 | override fun onNoDistributorFound() {
44 | // DO NOTHING
45 | }
46 | } */
47 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/utils/Tag.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.utils
2 |
3 | val Any.TAG: String
4 | get() {
5 | val tag = javaClass.simpleName
6 | return if (tag.length <= 23) tag else tag.substring(0, 23)
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/org/unifiedpush/example/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package org.unifiedpush.example.utils
2 |
3 | import android.os.Build
4 | import android.util.Base64
5 | import com.google.crypto.tink.subtle.EllipticCurves
6 | import java.security.KeyFactory
7 | import java.security.interfaces.ECPublicKey
8 | import java.security.spec.ECPublicKeySpec
9 |
10 | /**
11 | * Do we have implemented VAPID for the SDK of the phone ?
12 | */
13 | fun vapidImplementedForSdk(): Boolean {
14 | return Build.VERSION.SDK_INT >= 23
15 | }
16 |
17 | /**
18 | * Decode [ECPublicKey] from [String]
19 | */
20 | fun String.decodePubKey(): ECPublicKey {
21 | val point =
22 | EllipticCurves.pointDecode(
23 | EllipticCurves.CurveType.NIST_P256,
24 | EllipticCurves.PointFormatType.UNCOMPRESSED,
25 | this.b64decode()
26 | )
27 | val spec = EllipticCurves.getCurveSpec(EllipticCurves.CurveType.NIST_P256)
28 | return KeyFactory.getInstance("EC").generatePublic(ECPublicKeySpec(point, spec)) as ECPublicKey
29 | }
30 |
31 | /**
32 | * Encode [ECPublicKey] to [String]
33 | */
34 | fun ECPublicKey.encode(): String {
35 | val points =
36 | EllipticCurves.pointEncode(
37 | EllipticCurves.CurveType.NIST_P256,
38 | EllipticCurves.PointFormatType.UNCOMPRESSED,
39 | this.w
40 | )
41 | return points.b64encode()
42 | }
43 |
44 | /**
45 | * Base64 decode, url safe, no padding
46 | */
47 | fun String.b64decode(): ByteArray {
48 | return Base64.decode(
49 | this,
50 | Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
51 | )
52 | }
53 |
54 | /**
55 | * Base64 encode, url safe, no padding
56 | */
57 | fun ByteArray.b64encode(): String {
58 | return Base64.encode(
59 | this,
60 | Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
61 | ).toString(Charsets.UTF_8)
62 | }
63 |
64 | /**
65 | * Generate test page URL for parameters
66 | */
67 | fun genTestPageUrl(endpoint: String, p256dh: String, auth: String, vapid: String, showVapid: Boolean): String {
68 | var url = "https://unifiedpush.org/test_wp.html#endpoint=$endpoint&p256dh=$p256dh&auth=$auth"
69 | if (showVapid) {
70 | url += "&vapid=$vapid"
71 | }
72 | return url
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_notification.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #8445DC
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | UP-Example
3 | This is an example of an application using the UnifiedPush library.\n\nBy clicking on the registration button, you will have to choose between the distributors installed on your device the one that will provide the push notifications.
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = '2.0.21'
4 | repositories {
5 | google()
6 | mavenCentral()
7 | maven {
8 | url "https://plugins.gradle.org/m2/"
9 | content {
10 | includeModule 'org.jlleitschuh.gradle', 'ktlint-gradle'
11 | }
12 | }
13 | }
14 | dependencies {
15 | classpath 'com.android.tools.build:gradle:8.2.2'
16 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
17 | classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlin_version"
18 | classpath "org.jlleitschuh.gradle:ktlint-gradle:12.1.2"
19 | // NOTE: Do not place your application dependencies here; they belong
20 | // in the individual module build.gradle files
21 | }
22 | }
23 |
24 | allprojects {
25 | repositories {
26 | google()
27 | mavenCentral()
28 | }
29 | apply plugin: "org.jlleitschuh.gradle.ktlint"
30 | }
31 |
32 | tasks.register('clean', Delete) {
33 | delete rootProject.buildDir
34 | }
--------------------------------------------------------------------------------
/connector:
--------------------------------------------------------------------------------
1 | ../connector
--------------------------------------------------------------------------------
/connector_ui:
--------------------------------------------------------------------------------
1 | ../connector_ui
--------------------------------------------------------------------------------
/distributor:
--------------------------------------------------------------------------------
1 | ../embedded_fcm_distributor
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/10.txt:
--------------------------------------------------------------------------------
1 | * Add fastlane to publish on fdroid
2 | * Update dependencies
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/11.txt:
--------------------------------------------------------------------------------
1 | * Update app_name
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/12.txt:
--------------------------------------------------------------------------------
1 | * Toast unregistration
2 | * Set endpoint selectable
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/13.txt:
--------------------------------------------------------------------------------
1 | * Fix FCM Flavor
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/14.txt:
--------------------------------------------------------------------------------
1 | * Add screenshots for F-Droid
2 | * Update dependencies
3 | * Remove unused dependencies
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/15.txt:
--------------------------------------------------------------------------------
1 | - Use 2.0.0 libs
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/16.txt:
--------------------------------------------------------------------------------
1 | Bump dependencies
2 | Target SDK 33
3 | Accept user certificates
4 | Format byte message
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/17.txt:
--------------------------------------------------------------------------------
1 | Bump dependencies
2 | Print error when test fails
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/18.txt:
--------------------------------------------------------------------------------
1 | - Lower minSdkVersion
2 | - Update dependencies
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/19.txt:
--------------------------------------------------------------------------------
1 | - Add WebPush support
2 | - Add toggle to require BYTES_MESSAGE feature
3 | - Always go to the check activity if registered
4 | - Correct the description in the main activity
5 | - Minify the app
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/20.txt:
--------------------------------------------------------------------------------
1 | - Add WebPush support
2 | - Add toggle to require BYTES_MESSAGE feature
3 | - Always go to the check activity if registered
4 | - Correct the description in the main activity
5 | - Minify the app
6 | - Fix WebPush when selected during the first run
7 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/21.txt:
--------------------------------------------------------------------------------
1 | - Add WebPush support
2 | - Add toggle to require BYTES_MESSAGE feature
3 | - Always go to the check activity if registered
4 | - Correct the description in the main activity
5 | - Minify the app
6 | - Fix WebPush when selected during the first run
7 | - Fix URLDecode post parameters
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/22.txt:
--------------------------------------------------------------------------------
1 | - Fix activities lifecycle
2 | - Fix registration with FEATURE_BYTES_MESSAGE
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/23.txt:
--------------------------------------------------------------------------------
1 | - Check if distributor is still present before returning endpoint
2 | - Use the new UI lib
3 | - Bump dependencies
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/24.txt:
--------------------------------------------------------------------------------
1 | - Follow new specifications
2 | - Add tools to develop a distributor
3 | - New Ui
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/25.txt:
--------------------------------------------------------------------------------
1 | Upgrade android-connector with a fix
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/26.txt:
--------------------------------------------------------------------------------
1 | Upgrade android-connector with a fix
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/27.txt:
--------------------------------------------------------------------------------
1 | Add sub field in VAPID
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | This application is a generic application using UnifiedPush. It can be used to test your settings.
2 |
3 | You need to install an UnifiedPush distributor for this to work.
4 |
5 | More information at https://unifiedpush.org
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Example application using UnifiedPush library
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | UP-Example
2 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | android.defaults.buildfeatures.buildconfig=true
23 | android.nonTransitiveRClass=false
24 | android.nonFinalResIds=false
25 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UnifiedPush/android-example/f6923ce14445ac78866f0cb376211f4540481b88/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jan 08 16:14:12 CET 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | //toDevUILib//include ':connector_ui'
3 | //toDevMain//include ':connector'
4 | //toDevFcm//include ':distributor'
5 | rootProject.name = "UP-Example"
6 |
--------------------------------------------------------------------------------