├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .project ├── LICENSE ├── README.md ├── app.gif ├── app ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── gotify │ │ ├── MarkwonFactory.java │ │ ├── MissedMessageUtil.java │ │ ├── NotificationSupport.java │ │ ├── SSLSettings.java │ │ ├── Settings.java │ │ ├── Utils.java │ │ ├── api │ │ ├── Api.java │ │ ├── ApiException.java │ │ ├── Callback.java │ │ ├── CertUtils.java │ │ └── ClientFactory.java │ │ ├── init │ │ ├── BootCompletedReceiver.java │ │ └── InitializationActivity.java │ │ ├── log │ │ ├── Format.java │ │ ├── Log.java │ │ ├── LogsActivity.java │ │ └── UncaughtExceptionHandler.java │ │ ├── login │ │ ├── AdvancedDialog.java │ │ └── LoginActivity.java │ │ ├── messages │ │ ├── Extras.java │ │ ├── ListMessageAdapter.java │ │ ├── MessagesActivity.java │ │ └── provider │ │ │ ├── ApplicationHolder.java │ │ │ ├── MessageDeletion.java │ │ │ ├── MessageFacade.java │ │ │ ├── MessageImageCombiner.java │ │ │ ├── MessageRequester.java │ │ │ ├── MessageState.java │ │ │ ├── MessageStateHolder.java │ │ │ └── MessageWithImage.java │ │ ├── picasso │ │ ├── PicassoDataRequestHandler.java │ │ └── PicassoHandler.java │ │ ├── service │ │ ├── Constants.kt │ │ ├── MessagingDatabase.kt │ │ ├── PushNotification.kt │ │ ├── RegisterBroadcastReceiver.kt │ │ ├── WebSocketConnection.java │ │ └── WebSocketService.java │ │ ├── settings │ │ ├── SettingsActivity.java │ │ └── ThemeHelper.java │ │ └── sharing │ │ └── ShareActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_gotify.png │ ├── drawable-mdpi │ └── ic_gotify.png │ ├── drawable-xhdpi │ └── ic_gotify.png │ ├── drawable-xxhdpi │ └── ic_gotify.png │ ├── drawable-xxxhdpi │ └── ic_gotify.png │ ├── drawable │ ├── gotify.png │ ├── ic_alarm.xml │ ├── ic_bug_report.xml │ ├── ic_dashboard.xml │ ├── ic_delete.xml │ ├── ic_info.xml │ ├── ic_placeholder.xml │ ├── ic_power_setting.xml │ ├── ic_refresh.xml │ ├── ic_send.xml │ ├── ic_settings.xml │ └── side_nav_bar.xml │ ├── layout │ ├── activity_login.xml │ ├── activity_logs.xml │ ├── activity_messages.xml │ ├── activity_share.xml │ ├── advanced_settings_dialog.xml │ ├── app_bar_drawer.xml │ ├── message_item.xml │ ├── nav_header_drawer.xml │ ├── settings_activity.xml │ └── splash.xml │ ├── menu │ ├── logs_action.xml │ ├── messages_action.xml │ └── messages_menu.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── values-notnight │ └── colors.xml │ ├── values-v21 │ └── styles.xml │ ├── values │ ├── arrays.xml │ ├── colors.xml │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── network_security_config.xml │ └── root_preferences.xml ├── build.gradle ├── client ├── .gitignore ├── .swagger-codegen-ignore ├── .swagger-codegen │ └── VERSION ├── .travis.yml ├── README.md ├── build.gradle ├── build.sbt ├── docs │ ├── Application.md │ ├── ApplicationApi.md │ ├── Client.md │ ├── ClientApi.md │ ├── Error.md │ ├── Health.md │ ├── HealthApi.md │ ├── Message.md │ ├── MessageApi.md │ ├── PagedMessages.md │ ├── Paging.md │ ├── PluginApi.md │ ├── PluginConf.md │ ├── User.md │ ├── UserApi.md │ ├── UserPass.md │ ├── UserWithPass.md │ ├── VersionApi.md │ └── VersionInfo.md ├── git_push.sh ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pom.xml └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── github │ │ └── gotify │ │ └── client │ │ ├── ApiClient.java │ │ ├── CollectionFormats.java │ │ ├── JSON.java │ │ ├── StringUtil.java │ │ ├── api │ │ ├── ApplicationApi.java │ │ ├── ClientApi.java │ │ ├── HealthApi.java │ │ ├── MessageApi.java │ │ ├── PluginApi.java │ │ ├── UserApi.java │ │ └── VersionApi.java │ │ ├── auth │ │ ├── ApiKeyAuth.java │ │ ├── HttpBasicAuth.java │ │ ├── OAuth.java │ │ ├── OAuthFlow.java │ │ └── OAuthOkHttpClient.java │ │ └── model │ │ ├── Application.java │ │ ├── Client.java │ │ ├── Error.java │ │ ├── Health.java │ │ ├── Message.java │ │ ├── PagedMessages.java │ │ ├── Paging.java │ │ ├── PluginConf.java │ │ ├── User.java │ │ ├── UserPass.java │ │ ├── UserWithPass.java │ │ └── VersionInfo.java │ └── test │ └── java │ └── com │ └── github │ └── gotify │ └── client │ └── api │ ├── ApplicationApiTest.java │ ├── ClientApiTest.java │ ├── HealthApiTest.java │ ├── MessageApiTest.java │ ├── PluginApiTest.java │ ├── UserApiTest.java │ └── VersionApiTest.java ├── download-badge.png ├── fastlane └── metadata │ └── android │ └── en-US │ ├── changelogs │ ├── 10.txt │ ├── 11.txt │ ├── 7.txt │ ├── 8.txt │ └── 9.txt │ ├── full_description.txt │ ├── images │ └── phoneScreenshots │ │ └── 1.jpg │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lint.xml ├── settings.gradle └── swagger.config.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jmattheis 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: S1m 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://jmattheis.de/donate 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Build 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-java@v1 12 | with: 13 | java-version: 1.8 14 | - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} 15 | run: ./gradlew build --stacktrace 16 | - if: ${{ startsWith(github.ref, 'refs/tags/v') }} 17 | run: | 18 | export RELEASE_STORE_FILE=$(pwd)/gotfy-release-key.jks 19 | echo $RELEASE_KEY | base64 -d > $RELEASE_STORE_FILE 20 | ./gradlew -Psign build --stacktrace 21 | cp app/build/outputs/apk/release/app-release.apk app/build/outputs/apk/release/Gotify.apk 22 | env: 23 | RELEASE_KEY: ${{ secrets.RELEASE_KEY }} 24 | RELEASE_STORE_PASSWORD: ${{ secrets.STOREPASS }} 25 | RELEASE_KEY_ALIAS: unifiedpush 26 | RELEASE_KEY_PASSWORD: ${{ secrets.KEYPASS }} 27 | - if: ${{ startsWith(github.ref, 'refs/tags/v') }} 28 | uses: svenstaro/upload-release-action@v2 29 | with: 30 | repo_token: ${{ secrets.GITHUB_TOKEN }} 31 | file: app/build/outputs/apk/release/Gotify.apk 32 | tag: ${{ github.ref }} 33 | overwrite: true 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | build/ 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gotify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | We have stop maintaining this fork. If you wish to install an unifiedpush distributor, give a look at . The most similar to gotify at this moment is [ntfy](https://ntfy.sh). 4 | 5 | # Gotify-UP Android [![Build Status][github-action-badge]][github-action] [![FOSSA Status][fossa-badge]][fossa] [![latest release version][release-badge]][release] [![F-Droid][fdroid-badge]][fdroid] 6 | 7 | 8 | 9 | Gotify Android connects to [gotify/server](https://github.com/gotify/server) and shows push notifications on new messages. 10 | 11 | ## Features 12 | 13 | * show push notifications on new messages 14 | * view and delete messages 15 | 16 | ## Installation 17 | 18 | Download the apk or get the app via F-Droid or Google Play. 19 | 20 | [Get it on F-Droid][fdroid] 21 | [Get it on F-Droid][release] 22 | 23 | Google Play and the Google Play logo are trademarks of Google LLC. 24 | 25 | ### Disable battery optimization 26 | 27 | By default Android kills long running apps as they drain the battery. With enabled battery optimization, Gotify will be killed and you wont receive any notifications. 28 | 29 | Here is one way to disable battery optimization for Gotify. 30 | 31 | * Open "Settings" 32 | * Search for "Battery Optimization" 33 | * Find "Gotify-UP" and disable battery optimization 34 | 35 | See also https://dontkillmyapp.com for phone manufacturer specific instructions to disable battery optimizations. 36 | 37 | ### Minimize the Gotify foreground notification 38 | 39 | *Only possible for Android version >= 8* 40 | 41 | The foreground notification with content like `Listening to https://push.yourdomain.eu` can be manually minimized to be less intrusive: 42 | 43 | * Open Settings -> Apps -> Gotify-UP 44 | * Click Notifications 45 | * Click on `Gotify foreground notification` 46 | * Select a different "Behavior" or "Importance" (depends on your android version) 47 | * Restart Gotify 48 | 49 | ## Message Priorities 50 | 51 | | Notification | Gotify Priority| 52 | |- |-| 53 | | - | 0 | 54 | | Icon in notification bar | 1 - 3 | 55 | | Icon in notification bar + Sound | 4 - 7 | 56 | | Icon in notification bar + Sound + Vibration | 8 - 10 | 57 | 58 | ## Building 59 | 60 | Execute the following command to build the apk. 61 | ```bash 62 | $ ./gradlew build 63 | ``` 64 | 65 | ## Update client 66 | 67 | * Run `./gradlew generateSwaggerCode` 68 | * Discard changes to `client/build.gradle` (newer versions of dependencies) 69 | * Fix compile error in `client/src/main/java/com/github/gotify/client/auth/OAuthOkHttpClient.java` (caused by an updated dependency) 70 | * Delete `client/settings.gradle` (client is a gradle sub project and must not have a settings.gradle) 71 | * Commit changes 72 | 73 | ## Versioning 74 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the 75 | [tags on this repository](https://github.com/UnifiedPush/gotify-android/tags). 76 | 77 | ## License 78 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 79 | 80 | [github-action-badge]: https://github.com/UnifiedPush/gotify-android/workflows/Build/badge.svg 81 | [github-action]: https://github.com/UnifiedPush/gotify-android/actions?query=workflow%3ABuild 82 | [fdroid-badge]: https://img.shields.io/f-droid/v/com.github.gotify.up.svg 83 | [fdroid]: https://f-droid.org/de/packages/com.github.gotify.up/ 84 | [fossa-badge]: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fgotify%2Fandroid.svg?type=shield 85 | [fossa]: https://app.fossa.io/projects/git%2Bgithub.com%2Fgotify%2Fandroid 86 | [release-badge]: https://img.shields.io/github/release/UnifiedPush/gotify-android.svg 87 | [release]: https://github.com/UnifiedPush/gotify-android/releases/latest 88 | 89 | -------------------------------------------------------------------------------- /app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnifiedPush/gotify-android/567b4d3b8c1b02a9cab03a4348683936e775d1dd/app.gif -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.diffplug.gradle.spotless" version "3.26.1" 3 | } 4 | apply plugin: 'com.android.application' 5 | apply plugin: 'kotlin-android-extensions' 6 | apply plugin: 'kotlin-android' 7 | 8 | android { 9 | compileSdkVersion 30 10 | defaultConfig { 11 | applicationId "com.github.gotify.up" 12 | minSdkVersion 19 13 | targetSdkVersion 30 14 | versionCode 11 15 | versionName "2.2.0-UP2" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | vectorDrawables.useSupportLibrary = true 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility = '1.8' 27 | targetCompatibility = '1.8' 28 | } 29 | lintOptions { 30 | disable 'GoogleAppIndexingWarning' 31 | lintConfig file('../lint.xml') 32 | } 33 | packagingOptions { 34 | exclude 'META-INF/DEPENDENCIES' 35 | } 36 | } 37 | 38 | if (project.hasProperty('sign')) { 39 | android { 40 | signingConfigs { 41 | release { 42 | storeFile file(System.getenv("RELEASE_STORE_FILE")) 43 | storePassword System.getenv("RELEASE_STORE_PASSWORD") 44 | keyAlias System.getenv("RELEASE_KEY_ALIAS") 45 | keyPassword System.getenv("RELEASE_KEY_PASSWORD") 46 | } 47 | } 48 | } 49 | android.buildTypes.release.signingConfig android.signingConfigs.release 50 | } 51 | 52 | dependencies { 53 | implementation project(':client') 54 | implementation fileTree(dir: 'libs', include: ['*.jar']) 55 | implementation 'androidx.appcompat:appcompat:1.2.0' 56 | implementation 'com.google.android.material:material:1.3.0' 57 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 58 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 59 | implementation 'androidx.vectordrawable:vectordrawable:1.1.0' 60 | implementation 'androidx.preference:preference:1.1.1' 61 | 62 | implementation 'com.jakewharton:butterknife:10.2.3' 63 | annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3' 64 | implementation 'com.hypertrack:hyperlog:0.0.10' 65 | implementation 'com.squareup.picasso:picasso:2.71828' 66 | implementation 'io.noties.markwon:core:4.6.2' 67 | implementation 'io.noties.markwon:image-picasso:4.6.2' 68 | implementation 'io.noties.markwon:image:4.6.2' 69 | implementation 'io.noties.markwon:ext-tables:4.6.2' 70 | implementation "androidx.core:core-ktx:1.6.0" 71 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 72 | implementation 'io.noties.markwon:ext-strikethrough:4.6.2' 73 | } 74 | 75 | configurations { 76 | all { 77 | exclude group: 'org.json', module: 'json' 78 | } 79 | } 80 | 81 | spotless { 82 | java { 83 | target '**/*.java' 84 | googleJavaFormat().aosp() 85 | removeUnusedImports() 86 | importOrder('', 'static *') 87 | } 88 | } 89 | repositories { 90 | mavenCentral() 91 | } 92 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 41 | 45 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/MissedMessageUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify; 2 | 3 | import com.github.gotify.api.Api; 4 | import com.github.gotify.api.ApiException; 5 | import com.github.gotify.api.Callback; 6 | import com.github.gotify.client.api.MessageApi; 7 | import com.github.gotify.client.model.Message; 8 | import com.github.gotify.client.model.PagedMessages; 9 | import com.github.gotify.log.Log; 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | import static com.github.gotify.api.Callback.call; 15 | 16 | public class MissedMessageUtil { 17 | static final long NO_MESSAGES = 0; 18 | 19 | private final MessageApi api; 20 | 21 | public MissedMessageUtil(MessageApi api) { 22 | this.api = api; 23 | } 24 | 25 | public void lastReceivedMessage(Callback.SuccessCallback successCallback) { 26 | api.getMessages(1, 0L) 27 | .enqueue( 28 | call( 29 | (messages) -> { 30 | if (messages.getMessages().size() == 1) { 31 | successCallback.onSuccess( 32 | messages.getMessages().get(0).getId()); 33 | } else { 34 | successCallback.onSuccess(NO_MESSAGES); 35 | } 36 | }, 37 | (e) -> {})); 38 | } 39 | 40 | public List missingMessages(long till) { 41 | List result = new ArrayList<>(); 42 | try { 43 | 44 | Long since = null; 45 | while (true) { 46 | PagedMessages pagedMessages = Api.execute(api.getMessages(10, since)); 47 | List messages = pagedMessages.getMessages(); 48 | List filtered = filter(messages, till); 49 | result.addAll(filtered); 50 | if (messages.size() != filtered.size() 51 | || messages.size() == 0 52 | || pagedMessages.getPaging().getNext() == null) { 53 | break; 54 | } 55 | since = pagedMessages.getPaging().getSince(); 56 | } 57 | } catch (ApiException e) { 58 | Log.e("cannot retrieve missing messages", e); 59 | } 60 | Collections.reverse(result); 61 | return result; 62 | } 63 | 64 | private List filter(List messages, long till) { 65 | List result = new ArrayList<>(); 66 | 67 | for (Message message : messages) { 68 | if (message.getId() > till) { 69 | result.add(message); 70 | } else { 71 | break; 72 | } 73 | } 74 | 75 | return result; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/SSLSettings.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify; 2 | 3 | public class SSLSettings { 4 | public boolean validateSSL; 5 | public String cert; 6 | 7 | public SSLSettings(boolean validateSSL, String cert) { 8 | this.validateSSL = validateSSL; 9 | this.cert = cert; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/Settings.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import com.github.gotify.client.model.User; 6 | 7 | public class Settings { 8 | private final SharedPreferences sharedPreferences; 9 | 10 | public Settings(Context context) { 11 | sharedPreferences = context.getSharedPreferences("gotify", Context.MODE_PRIVATE); 12 | } 13 | 14 | public void url(String url) { 15 | sharedPreferences.edit().putString("url", url).apply(); 16 | } 17 | 18 | public String url() { 19 | return sharedPreferences.getString("url", null); 20 | } 21 | 22 | public boolean tokenExists() { 23 | return token() != null; 24 | } 25 | 26 | public String token() { 27 | return sharedPreferences.getString("token", null); 28 | } 29 | 30 | public void token(String token) { 31 | sharedPreferences.edit().putString("token", token).apply(); 32 | } 33 | 34 | public void clear() { 35 | url(null); 36 | token(null); 37 | validateSSL(true); 38 | cert(null); 39 | } 40 | 41 | public void user(String name, boolean admin) { 42 | sharedPreferences.edit().putString("username", name).putBoolean("admin", admin).apply(); 43 | } 44 | 45 | public User user() { 46 | String username = sharedPreferences.getString("username", null); 47 | boolean admin = sharedPreferences.getBoolean("admin", false); 48 | if (username != null) { 49 | return new User().name(username).admin(admin); 50 | } else { 51 | return new User().name("UNKNOWN").admin(false); 52 | } 53 | } 54 | 55 | public String serverVersion() { 56 | return sharedPreferences.getString("version", "UNKNOWN"); 57 | } 58 | 59 | public void serverVersion(String version) { 60 | sharedPreferences.edit().putString("version", version).apply(); 61 | } 62 | 63 | private boolean validateSSL() { 64 | return sharedPreferences.getBoolean("validateSSL", true); 65 | } 66 | 67 | public void validateSSL(boolean validateSSL) { 68 | sharedPreferences.edit().putBoolean("validateSSL", validateSSL).apply(); 69 | } 70 | 71 | private String cert() { 72 | return sharedPreferences.getString("cert", null); 73 | } 74 | 75 | public void cert(String cert) { 76 | sharedPreferences.edit().putString("cert", cert).apply(); 77 | } 78 | 79 | public SSLSettings sslSettings() { 80 | return new SSLSettings(validateSSL(), cert()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/Utils.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify; 2 | 3 | import android.app.Activity; 4 | import android.content.res.Resources; 5 | import android.graphics.Bitmap; 6 | import android.graphics.drawable.BitmapDrawable; 7 | import android.graphics.drawable.Drawable; 8 | import android.text.format.DateUtils; 9 | import android.view.View; 10 | import androidx.annotation.NonNull; 11 | import com.github.gotify.client.JSON; 12 | import com.github.gotify.log.Log; 13 | import com.google.android.material.snackbar.Snackbar; 14 | import com.google.gson.Gson; 15 | import com.squareup.picasso.Picasso; 16 | import com.squareup.picasso.Target; 17 | import java.io.BufferedReader; 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.io.InputStreamReader; 21 | import java.net.MalformedURLException; 22 | import java.net.URI; 23 | import java.net.URISyntaxException; 24 | import java.net.URL; 25 | import okio.Buffer; 26 | import org.threeten.bp.OffsetDateTime; 27 | 28 | public class Utils { 29 | public static final Gson JSON = new JSON().getGson(); 30 | 31 | public static void showSnackBar(Activity activity, String message) { 32 | View rootView = activity.getWindow().getDecorView().findViewById(android.R.id.content); 33 | Snackbar.make(rootView, message, Snackbar.LENGTH_SHORT).show(); 34 | } 35 | 36 | public static int longToInt(long value) { 37 | return (int) (value % Integer.MAX_VALUE); 38 | } 39 | 40 | public static String dateToRelative(OffsetDateTime data) { 41 | long time = data.toInstant().toEpochMilli(); 42 | long now = System.currentTimeMillis(); 43 | return DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS) 44 | .toString(); 45 | } 46 | 47 | public static String resolveAbsoluteUrl(String baseURL, String target) { 48 | if (target == null) { 49 | return null; 50 | } 51 | try { 52 | URI targetUri = new URI(target); 53 | if (targetUri.isAbsolute()) { 54 | return target; 55 | } 56 | return new URL(new URL(baseURL), target).toString(); 57 | } catch (MalformedURLException | URISyntaxException e) { 58 | Log.e("Could not resolve absolute url", e); 59 | return target; 60 | } 61 | } 62 | 63 | public static Target toDrawable(Resources resources, DrawableReceiver drawableReceiver) { 64 | return new Target() { 65 | @Override 66 | public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { 67 | drawableReceiver.loaded(new BitmapDrawable(resources, bitmap)); 68 | } 69 | 70 | @Override 71 | public void onBitmapFailed(Exception e, Drawable errorDrawable) { 72 | Log.e("Bitmap failed", e); 73 | } 74 | 75 | @Override 76 | public void onPrepareLoad(Drawable placeHolderDrawable) {} 77 | }; 78 | } 79 | 80 | public static String readFileFromStream(@NonNull InputStream inputStream) { 81 | StringBuilder sb = new StringBuilder(); 82 | String currentLine; 83 | 84 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { 85 | while ((currentLine = reader.readLine()) != null) { 86 | sb.append(currentLine).append("\n"); 87 | } 88 | } catch (IOException e) { 89 | throw new IllegalArgumentException("failed to read input"); 90 | } 91 | 92 | return sb.toString(); 93 | } 94 | 95 | public interface DrawableReceiver { 96 | void loaded(Drawable drawable); 97 | } 98 | 99 | public static InputStream stringToInputStream(String str) { 100 | if (str == null) return null; 101 | return new Buffer().writeUtf8(str).inputStream(); 102 | } 103 | 104 | public static T first(T[] data) { 105 | if (data.length != 1) { 106 | throw new IllegalArgumentException("must be one element"); 107 | } 108 | return data[0]; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/api/Api.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.api; 2 | 3 | import java.io.IOException; 4 | import retrofit2.Call; 5 | import retrofit2.Response; 6 | 7 | public class Api { 8 | public static T execute(Call call) throws ApiException { 9 | try { 10 | Response response = call.execute(); 11 | 12 | if (response.isSuccessful()) { 13 | return response.body(); 14 | } else { 15 | throw new ApiException(response); 16 | } 17 | } catch (IOException e) { 18 | throw new ApiException(e); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/api/ApiException.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.api; 2 | 3 | import java.io.IOException; 4 | import java.util.Locale; 5 | import retrofit2.Response; 6 | 7 | public final class ApiException extends Exception { 8 | 9 | private String body; 10 | private int code; 11 | 12 | ApiException(Response response) { 13 | super("Api Error", null); 14 | try { 15 | this.body = response.errorBody() != null ? response.errorBody().string() : ""; 16 | } catch (IOException e) { 17 | this.body = "Error while getting error body :("; 18 | } 19 | this.code = response.code(); 20 | } 21 | 22 | ApiException(Throwable cause) { 23 | super("Request failed.", cause); 24 | this.body = ""; 25 | this.code = 0; 26 | } 27 | 28 | public String body() { 29 | return body; 30 | } 31 | 32 | public int code() { 33 | return code; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return String.format( 39 | Locale.ENGLISH, 40 | "Code(%d) Response: %s", 41 | code(), 42 | body().substring(0, Math.min(body().length(), 200))); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/api/Callback.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.api; 2 | 3 | import android.app.Activity; 4 | import com.github.gotify.log.Log; 5 | import retrofit2.Call; 6 | import retrofit2.Response; 7 | 8 | public class Callback { 9 | private final SuccessCallback onSuccess; 10 | private final ErrorCallback onError; 11 | 12 | private Callback(SuccessCallback onSuccess, ErrorCallback onError) { 13 | this.onSuccess = onSuccess; 14 | this.onError = onError; 15 | } 16 | 17 | public static retrofit2.Callback callInUI( 18 | Activity context, SuccessCallback onSuccess, ErrorCallback onError) { 19 | return call( 20 | (data) -> context.runOnUiThread(() -> onSuccess.onSuccess(data)), 21 | (e) -> context.runOnUiThread(() -> onError.onError(e))); 22 | } 23 | 24 | public static retrofit2.Callback call() { 25 | return call((e) -> {}, (e) -> {}); 26 | } 27 | 28 | public static retrofit2.Callback call( 29 | SuccessCallback onSuccess, ErrorCallback onError) { 30 | return new RetrofitCallback<>(merge(of(onSuccess, onError), errorCallback())); 31 | } 32 | 33 | private static Callback of(SuccessCallback onSuccess, ErrorCallback onError) { 34 | return new Callback<>(onSuccess, onError); 35 | } 36 | 37 | private static Callback errorCallback() { 38 | return new Callback<>((ignored) -> {}, (error) -> Log.e("Error while api call", error)); 39 | } 40 | 41 | private static Callback merge(Callback left, Callback right) { 42 | return new Callback<>( 43 | (data) -> { 44 | left.onSuccess.onSuccess(data); 45 | right.onSuccess.onSuccess(data); 46 | }, 47 | (error) -> { 48 | left.onError.onError(error); 49 | right.onError.onError(error); 50 | }); 51 | } 52 | 53 | public interface SuccessCallback { 54 | void onSuccess(T data); 55 | } 56 | 57 | public interface ErrorCallback { 58 | void onError(ApiException t); 59 | } 60 | 61 | private static final class RetrofitCallback implements retrofit2.Callback { 62 | 63 | private Callback callback; 64 | 65 | private RetrofitCallback(Callback callback) { 66 | this.callback = callback; 67 | } 68 | 69 | @Override 70 | public void onResponse(Call call, Response response) { 71 | if (response.isSuccessful()) { 72 | callback.onSuccess.onSuccess(response.body()); 73 | } else { 74 | callback.onError.onError(new ApiException(response)); 75 | } 76 | } 77 | 78 | @Override 79 | public void onFailure(Call call, Throwable t) { 80 | callback.onError.onError(new ApiException(t)); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/api/CertUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.api; 2 | 3 | import android.annotation.SuppressLint; 4 | import com.github.gotify.SSLSettings; 5 | import com.github.gotify.Utils; 6 | import com.github.gotify.log.Log; 7 | import java.io.IOException; 8 | import java.security.GeneralSecurityException; 9 | import java.security.KeyStore; 10 | import java.security.SecureRandom; 11 | import java.security.cert.Certificate; 12 | import java.security.cert.CertificateFactory; 13 | import java.security.cert.X509Certificate; 14 | import java.util.Collection; 15 | import javax.net.ssl.KeyManager; 16 | import javax.net.ssl.SSLContext; 17 | import javax.net.ssl.TrustManager; 18 | import javax.net.ssl.TrustManagerFactory; 19 | import javax.net.ssl.X509TrustManager; 20 | import okhttp3.OkHttpClient; 21 | 22 | public class CertUtils { 23 | private static final X509TrustManager trustAll = 24 | new X509TrustManager() { 25 | @SuppressLint("TrustAllX509TrustManager") 26 | @Override 27 | public void checkClientTrusted(X509Certificate[] chain, String authType) {} 28 | 29 | @SuppressLint("TrustAllX509TrustManager") 30 | @Override 31 | public void checkServerTrusted(X509Certificate[] chain, String authType) {} 32 | 33 | @Override 34 | public X509Certificate[] getAcceptedIssuers() { 35 | return new X509Certificate[] {}; 36 | } 37 | }; 38 | 39 | public static Certificate parseCertificate(String cert) { 40 | try { 41 | CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); 42 | 43 | return certificateFactory.generateCertificate(Utils.stringToInputStream(cert)); 44 | } catch (Exception e) { 45 | throw new IllegalArgumentException("certificate is invalid"); 46 | } 47 | } 48 | 49 | public static void applySslSettings(OkHttpClient.Builder builder, SSLSettings settings) { 50 | // Modified from ApiClient.applySslSettings in the client package. 51 | 52 | try { 53 | if (!settings.validateSSL) { 54 | SSLContext context = SSLContext.getInstance("TLS"); 55 | context.init( 56 | new KeyManager[] {}, new TrustManager[] {trustAll}, new SecureRandom()); 57 | builder.sslSocketFactory(context.getSocketFactory(), trustAll); 58 | builder.hostnameVerifier((a, b) -> true); 59 | return; 60 | } 61 | 62 | if (settings.cert != null) { 63 | TrustManager[] trustManagers = certToTrustManager(settings.cert); 64 | 65 | if (trustManagers != null && trustManagers.length > 0) { 66 | SSLContext context = SSLContext.getInstance("TLS"); 67 | context.init(new KeyManager[] {}, trustManagers, new SecureRandom()); 68 | builder.sslSocketFactory( 69 | context.getSocketFactory(), (X509TrustManager) trustManagers[0]); 70 | } 71 | } 72 | } catch (Exception e) { 73 | // We shouldn't have issues since the cert is verified on login. 74 | Log.e("Failed to apply SSL settings", e); 75 | } 76 | } 77 | 78 | private static TrustManager[] certToTrustManager(String cert) throws GeneralSecurityException { 79 | CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); 80 | Collection certificates = 81 | certificateFactory.generateCertificates(Utils.stringToInputStream(cert)); 82 | if (certificates.isEmpty()) { 83 | throw new IllegalArgumentException("expected non-empty set of trusted certificates"); 84 | } 85 | KeyStore caKeyStore = newEmptyKeyStore(); 86 | int index = 0; 87 | for (Certificate certificate : certificates) { 88 | String certificateAlias = "ca" + Integer.toString(index++); 89 | caKeyStore.setCertificateEntry(certificateAlias, certificate); 90 | } 91 | TrustManagerFactory trustManagerFactory = 92 | TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 93 | trustManagerFactory.init(caKeyStore); 94 | return trustManagerFactory.getTrustManagers(); 95 | } 96 | 97 | private static KeyStore newEmptyKeyStore() throws GeneralSecurityException { 98 | try { 99 | KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 100 | keyStore.load(null, null); 101 | return keyStore; 102 | } catch (IOException e) { 103 | throw new AssertionError(e); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/api/ClientFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.api; 2 | 3 | import com.github.gotify.SSLSettings; 4 | import com.github.gotify.Settings; 5 | import com.github.gotify.client.ApiClient; 6 | import com.github.gotify.client.api.UserApi; 7 | import com.github.gotify.client.api.VersionApi; 8 | import com.github.gotify.client.auth.ApiKeyAuth; 9 | import com.github.gotify.client.auth.HttpBasicAuth; 10 | 11 | public class ClientFactory { 12 | public static com.github.gotify.client.ApiClient unauthorized( 13 | String baseUrl, SSLSettings sslSettings) { 14 | return defaultClient(new String[0], baseUrl + "/", sslSettings); 15 | } 16 | 17 | public static ApiClient basicAuth( 18 | String baseUrl, SSLSettings sslSettings, String username, String password) { 19 | ApiClient client = defaultClient(new String[] {"basicAuth"}, baseUrl + "/", sslSettings); 20 | HttpBasicAuth auth = (HttpBasicAuth) client.getApiAuthorizations().get("basicAuth"); 21 | auth.setUsername(username); 22 | auth.setPassword(password); 23 | return client; 24 | } 25 | 26 | public static ApiClient clientToken(String baseUrl, SSLSettings sslSettings, String token) { 27 | ApiClient client = 28 | defaultClient(new String[] {"clientTokenHeader"}, baseUrl + "/", sslSettings); 29 | ApiKeyAuth tokenAuth = (ApiKeyAuth) client.getApiAuthorizations().get("clientTokenHeader"); 30 | tokenAuth.setApiKey(token); 31 | return client; 32 | } 33 | 34 | public static VersionApi versionApi(String baseUrl, SSLSettings sslSettings) { 35 | return unauthorized(baseUrl, sslSettings).createService(VersionApi.class); 36 | } 37 | 38 | public static UserApi userApiWithToken(Settings settings) { 39 | return clientToken(settings.url(), settings.sslSettings(), settings.token()) 40 | .createService(UserApi.class); 41 | } 42 | 43 | private static ApiClient defaultClient( 44 | String[] authentications, String baseUrl, SSLSettings sslSettings) { 45 | ApiClient client = new ApiClient(authentications); 46 | CertUtils.applySslSettings(client.getOkBuilder(), sslSettings); 47 | client.getAdapterBuilder().baseUrl(baseUrl); 48 | return client; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/init/BootCompletedReceiver.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.init; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Build; 7 | import com.github.gotify.Settings; 8 | import com.github.gotify.service.WebSocketService; 9 | 10 | public class BootCompletedReceiver extends BroadcastReceiver { 11 | 12 | @Override 13 | public void onReceive(Context context, Intent intent) { 14 | Settings settings = new Settings(context); 15 | 16 | if (!settings.tokenExists()) { 17 | return; 18 | } 19 | 20 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 21 | context.startForegroundService(new Intent(context, WebSocketService.class)); 22 | } else { 23 | context.startService(new Intent(context, WebSocketService.class)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/log/Format.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.log; 2 | 3 | import android.content.Context; 4 | import com.hypertrack.hyperlog.LogFormat; 5 | import java.util.Locale; 6 | 7 | public class Format extends LogFormat { 8 | Format(Context context) { 9 | super(context); 10 | } 11 | 12 | @Override 13 | public String getFormattedLogMessage( 14 | String logLevelName, 15 | String tag, 16 | String message, 17 | String timeStamp, 18 | String senderName, 19 | String osVersion, 20 | String deviceUUID) { 21 | return String.format(Locale.ENGLISH, "%s %s: %s", timeStamp, logLevelName, message); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/log/Log.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.log; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | import com.hypertrack.hyperlog.HyperLog; 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | public class Log { 10 | private static String TAG = "gotify"; 11 | 12 | public static void init(Context content) { 13 | HyperLog.initialize(content, new Format(content)); 14 | HyperLog.setLogLevel(android.util.Log.INFO); // TODO configurable 15 | } 16 | 17 | public static String get() { 18 | List logs = HyperLog.getDeviceLogsAsStringList(false); 19 | Collections.reverse(logs); 20 | return TextUtils.join("\n", logs.subList(0, Math.min(200, logs.size()))); 21 | } 22 | 23 | public static void e(String message) { 24 | HyperLog.e(TAG, message); 25 | } 26 | 27 | public static void e(String message, Throwable e) { 28 | HyperLog.e(TAG, message + '\n' + android.util.Log.getStackTraceString(e)); 29 | } 30 | 31 | public static void i(String message) { 32 | HyperLog.i(TAG, message); 33 | } 34 | 35 | public static void i(String message, Throwable e) { 36 | HyperLog.i(TAG, message + '\n' + android.util.Log.getStackTraceString(e)); 37 | } 38 | 39 | public static void w(String message) { 40 | HyperLog.w(TAG, message); 41 | } 42 | 43 | public static void w(String message, Throwable e) { 44 | HyperLog.w(TAG, message + '\n' + android.util.Log.getStackTraceString(e)); 45 | } 46 | 47 | public static void clear() { 48 | HyperLog.deleteLogs(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/log/LogsActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.log; 2 | 3 | import android.content.ClipData; 4 | import android.content.ClipboardManager; 5 | import android.content.Context; 6 | import android.os.AsyncTask; 7 | import android.os.Bundle; 8 | import android.os.Handler; 9 | import android.view.Menu; 10 | import android.view.MenuItem; 11 | import android.widget.TextView; 12 | import androidx.appcompat.app.ActionBar; 13 | import androidx.appcompat.app.AppCompatActivity; 14 | import com.github.gotify.R; 15 | import com.github.gotify.Utils; 16 | 17 | public class LogsActivity extends AppCompatActivity { 18 | 19 | private Handler handler = new Handler(); 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_logs); 25 | Log.i("Entering " + getClass().getSimpleName()); 26 | updateLogs(); 27 | setSupportActionBar(findViewById(R.id.toolbar)); 28 | ActionBar actionBar = getSupportActionBar(); 29 | if (actionBar != null) { 30 | actionBar.setDisplayHomeAsUpEnabled(true); 31 | actionBar.setDisplayShowCustomEnabled(true); 32 | } 33 | } 34 | 35 | private void updateLogs() { 36 | new RefreshLogs().execute(); 37 | if (!isDestroyed()) { 38 | handler.postDelayed(this::updateLogs, 5000); 39 | } 40 | } 41 | 42 | @Override 43 | public boolean onCreateOptionsMenu(Menu menu) { 44 | getMenuInflater().inflate(R.menu.logs_action, menu); 45 | return super.onCreateOptionsMenu(menu); 46 | } 47 | 48 | @Override 49 | public boolean onOptionsItemSelected(MenuItem item) { 50 | if (item.getItemId() == android.R.id.home) { 51 | finish(); 52 | } 53 | if (item.getItemId() == R.id.action_delete_logs) { 54 | Log.clear(); 55 | } 56 | if (item.getItemId() == R.id.action_copy_logs) { 57 | TextView content = findViewById(R.id.log_content); 58 | ClipboardManager clipboardManager = 59 | (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); 60 | ClipData clipData = ClipData.newPlainText("GotifyLog", content.getText().toString()); 61 | clipboardManager.setPrimaryClip(clipData); 62 | Utils.showSnackBar(this, getString(R.string.logs_copied)); 63 | } 64 | return super.onOptionsItemSelected(item); 65 | } 66 | 67 | class RefreshLogs extends AsyncTask { 68 | 69 | @Override 70 | protected String doInBackground(Void... voids) { 71 | return com.github.gotify.log.Log.get(); 72 | } 73 | 74 | @Override 75 | protected void onPostExecute(String s) { 76 | TextView content = findViewById(R.id.log_content); 77 | if (content.getSelectionStart() == content.getSelectionEnd()) { 78 | content.setText(s); 79 | } 80 | super.onPostExecute(s); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/log/UncaughtExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.log; 2 | 3 | public class UncaughtExceptionHandler { 4 | public static void registerCurrentThread() { 5 | Thread.setDefaultUncaughtExceptionHandler((t, e) -> Log.e("uncaught exception", e)); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/login/AdvancedDialog.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.login; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.widget.Button; 8 | import android.widget.CheckBox; 9 | import android.widget.CompoundButton; 10 | import android.widget.TextView; 11 | import androidx.annotation.Nullable; 12 | import butterknife.BindView; 13 | import butterknife.ButterKnife; 14 | import com.github.gotify.R; 15 | 16 | class AdvancedDialog { 17 | 18 | private Context context; 19 | private ViewHolder holder; 20 | private CompoundButton.OnCheckedChangeListener onCheckedChangeListener; 21 | private Runnable onClickSelectCaCertificate; 22 | private Runnable onClickRemoveCaCertificate; 23 | 24 | AdvancedDialog(Context context) { 25 | this.context = context; 26 | } 27 | 28 | AdvancedDialog onDisableSSLChanged( 29 | CompoundButton.OnCheckedChangeListener onCheckedChangeListener) { 30 | this.onCheckedChangeListener = onCheckedChangeListener; 31 | return this; 32 | } 33 | 34 | AdvancedDialog onClickSelectCaCertificate(Runnable onClickSelectCaCertificate) { 35 | this.onClickSelectCaCertificate = onClickSelectCaCertificate; 36 | return this; 37 | } 38 | 39 | AdvancedDialog onClickRemoveCaCertificate(Runnable onClickRemoveCaCertificate) { 40 | this.onClickRemoveCaCertificate = onClickRemoveCaCertificate; 41 | return this; 42 | } 43 | 44 | AdvancedDialog show(boolean disableSSL, @Nullable String selectedCertificate) { 45 | 46 | View dialogView = 47 | LayoutInflater.from(context).inflate(R.layout.advanced_settings_dialog, null); 48 | holder = new ViewHolder(dialogView); 49 | holder.disableSSL.setChecked(disableSSL); 50 | holder.disableSSL.setOnCheckedChangeListener(onCheckedChangeListener); 51 | 52 | if (selectedCertificate == null) { 53 | showSelectCACertificate(); 54 | } else { 55 | showRemoveCACertificate(selectedCertificate); 56 | } 57 | 58 | new AlertDialog.Builder(context) 59 | .setView(dialogView) 60 | .setTitle(R.string.advanced_settings) 61 | .setPositiveButton(context.getString(R.string.done), (ignored, ignored2) -> {}) 62 | .show(); 63 | return this; 64 | } 65 | 66 | private void showSelectCACertificate() { 67 | holder.toggleCaCert.setText(R.string.select_ca_certificate); 68 | holder.toggleCaCert.setOnClickListener((a) -> onClickSelectCaCertificate.run()); 69 | holder.selectedCaCertificate.setText(R.string.no_certificate_selected); 70 | } 71 | 72 | void showRemoveCACertificate(String certificate) { 73 | holder.toggleCaCert.setText(R.string.remove_ca_certificate); 74 | holder.toggleCaCert.setOnClickListener( 75 | (a) -> { 76 | showSelectCACertificate(); 77 | onClickRemoveCaCertificate.run(); 78 | }); 79 | holder.selectedCaCertificate.setText(certificate); 80 | } 81 | 82 | class ViewHolder { 83 | @BindView(R.id.disableSSL) 84 | CheckBox disableSSL; 85 | 86 | @BindView(R.id.toggle_ca_cert) 87 | Button toggleCaCert; 88 | 89 | @BindView(R.id.seleceted_ca_cert) 90 | TextView selectedCaCertificate; 91 | 92 | ViewHolder(View view) { 93 | ButterKnife.bind(this, view); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/messages/Extras.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.messages; 2 | 3 | import com.github.gotify.client.model.Message; 4 | import java.util.Map; 5 | 6 | public final class Extras { 7 | private Extras() {} 8 | 9 | public static boolean useMarkdown(Message message) { 10 | return useMarkdown(message.getExtras()); 11 | } 12 | 13 | public static boolean useMarkdown(Map extras) { 14 | if (extras == null) { 15 | return false; 16 | } 17 | 18 | Object display = extras.get("client::display"); 19 | if (!(display instanceof Map)) { 20 | return false; 21 | } 22 | 23 | return "text/markdown".equals(((Map) display).get("contentType")); 24 | } 25 | 26 | public static T getNestedValue(Class clazz, Message message, String... keys) { 27 | return getNestedValue(clazz, message.getExtras(), keys); 28 | } 29 | 30 | public static T getNestedValue(Class clazz, Map extras, String... keys) { 31 | Object value = extras; 32 | 33 | for (String key : keys) { 34 | if (value == null) { 35 | return null; 36 | } 37 | 38 | if (!(value instanceof Map)) { 39 | return null; 40 | } 41 | 42 | value = ((Map) value).get(key); 43 | } 44 | 45 | if (!clazz.isInstance(value)) { 46 | return null; 47 | } 48 | 49 | return clazz.cast(value); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/messages/provider/ApplicationHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.messages.provider; 2 | 3 | import android.app.Activity; 4 | import com.github.gotify.Utils; 5 | import com.github.gotify.api.ApiException; 6 | import com.github.gotify.api.Callback; 7 | import com.github.gotify.client.ApiClient; 8 | import com.github.gotify.client.api.ApplicationApi; 9 | import com.github.gotify.client.model.Application; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | public class ApplicationHolder { 14 | private List state; 15 | private Runnable onUpdate; 16 | private Runnable onUpdateFailed; 17 | private Activity activity; 18 | private ApiClient client; 19 | 20 | public ApplicationHolder(Activity activity, ApiClient client) { 21 | this.activity = activity; 22 | this.client = client; 23 | } 24 | 25 | public void requestIfMissing() { 26 | if (state == null) { 27 | request(); 28 | } 29 | } 30 | 31 | public void request() { 32 | client.createService(ApplicationApi.class) 33 | .getApps() 34 | .enqueue(Callback.callInUI(activity, this::onReceiveApps, this::onFailedApps)); 35 | } 36 | 37 | private void onReceiveApps(List apps) { 38 | state = apps; 39 | if (onUpdate != null) onUpdate.run(); 40 | } 41 | 42 | private void onFailedApps(ApiException e) { 43 | Utils.showSnackBar(activity, "Could not request applications, see logs."); 44 | if (onUpdateFailed != null) onUpdateFailed.run(); 45 | } 46 | 47 | public List get() { 48 | return state == null ? Collections.emptyList() : state; 49 | } 50 | 51 | public void onUpdate(Runnable runnable) { 52 | this.onUpdate = runnable; 53 | } 54 | 55 | public void onUpdateFailed(Runnable runnable) { 56 | this.onUpdateFailed = runnable; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/messages/provider/MessageDeletion.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.messages.provider; 2 | 3 | import com.github.gotify.client.model.Message; 4 | 5 | public final class MessageDeletion { 6 | private final Message message; 7 | private final int allPosition; 8 | private final int appPosition; 9 | 10 | public MessageDeletion(Message message, int allPosition, int appPosition) { 11 | this.message = message; 12 | this.allPosition = allPosition; 13 | this.appPosition = appPosition; 14 | } 15 | 16 | public int getAllPosition() { 17 | return allPosition; 18 | } 19 | 20 | public int getAppPosition() { 21 | return appPosition; 22 | } 23 | 24 | public Message getMessage() { 25 | return message; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/messages/provider/MessageFacade.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.messages.provider; 2 | 3 | import com.github.gotify.client.api.MessageApi; 4 | import com.github.gotify.client.model.Message; 5 | import com.github.gotify.client.model.PagedMessages; 6 | import java.util.List; 7 | 8 | public class MessageFacade { 9 | private final ApplicationHolder applicationHolder; 10 | private final MessageRequester requester; 11 | private final MessageStateHolder state; 12 | private final MessageImageCombiner combiner; 13 | 14 | public MessageFacade(MessageApi api, ApplicationHolder applicationHolder) { 15 | this.applicationHolder = applicationHolder; 16 | this.requester = new MessageRequester(api); 17 | this.combiner = new MessageImageCombiner(); 18 | this.state = new MessageStateHolder(); 19 | } 20 | 21 | public synchronized List get(long appId) { 22 | return combiner.combine(state.state(appId).messages, applicationHolder.get()); 23 | } 24 | 25 | public synchronized void addMessages(List messages) { 26 | for (Message message : messages) { 27 | state.newMessage(message); 28 | } 29 | } 30 | 31 | public synchronized List loadMore(long appId) { 32 | MessageState state = this.state.state(appId); 33 | if (state.hasNext || !state.loaded) { 34 | PagedMessages pagedMessages = requester.loadMore(state); 35 | this.state.newMessages(appId, pagedMessages); 36 | } 37 | return get(appId); 38 | } 39 | 40 | public synchronized void loadMoreIfNotPresent(long appId) { 41 | MessageState state = this.state.state(appId); 42 | if (!state.loaded) { 43 | loadMore(appId); 44 | } 45 | } 46 | 47 | public synchronized void clear() { 48 | this.state.clear(); 49 | } 50 | 51 | public long getLastReceivedMessage() { 52 | return state.getLastReceivedMessage(); 53 | } 54 | 55 | public synchronized void deleteLocal(Message message) { 56 | // If there is already a deletion pending, that one should be executed before scheduling the 57 | // next deletion. 58 | if (this.state.deletionPending()) commitDelete(); 59 | this.state.deleteMessage(message); 60 | } 61 | 62 | public synchronized void commitDelete() { 63 | if (this.state.deletionPending()) { 64 | MessageDeletion deletion = this.state.purgePendingDeletion(); 65 | this.requester.asyncRemoveMessage(deletion.getMessage()); 66 | } 67 | } 68 | 69 | public synchronized MessageDeletion undoDeleteLocal() { 70 | return this.state.undoPendingDeletion(); 71 | } 72 | 73 | public synchronized boolean deleteAll(long appId) { 74 | boolean success = this.requester.deleteAll(appId); 75 | this.state.deleteAll(appId); 76 | return success; 77 | } 78 | 79 | public synchronized boolean canLoadMore(long appId) { 80 | return state.state(appId).hasNext; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/messages/provider/MessageImageCombiner.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.messages.provider; 2 | 3 | import com.github.gotify.client.model.Application; 4 | import com.github.gotify.client.model.Message; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | public class MessageImageCombiner { 11 | 12 | List combine(List messages, List applications) { 13 | Map appIdToImage = appIdToImage(applications); 14 | 15 | List result = new ArrayList<>(); 16 | 17 | for (Message message : messages) { 18 | MessageWithImage messageWithImage = new MessageWithImage(); 19 | 20 | messageWithImage.message = message; 21 | messageWithImage.image = appIdToImage.get(message.getAppid()); 22 | 23 | result.add(messageWithImage); 24 | } 25 | 26 | return result; 27 | } 28 | 29 | public static Map appIdToImage(List applications) { 30 | Map map = new ConcurrentHashMap<>(); 31 | for (Application app : applications) { 32 | map.put(app.getId(), app.getImage()); 33 | } 34 | return map; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/messages/provider/MessageRequester.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.messages.provider; 2 | 3 | import com.github.gotify.api.Api; 4 | import com.github.gotify.api.ApiException; 5 | import com.github.gotify.api.Callback; 6 | import com.github.gotify.client.api.MessageApi; 7 | import com.github.gotify.client.model.Message; 8 | import com.github.gotify.client.model.PagedMessages; 9 | import com.github.gotify.log.Log; 10 | 11 | class MessageRequester { 12 | private static final Integer LIMIT = 100; 13 | private MessageApi messageApi; 14 | 15 | MessageRequester(MessageApi messageApi) { 16 | this.messageApi = messageApi; 17 | } 18 | 19 | PagedMessages loadMore(MessageState state) { 20 | try { 21 | Log.i("Loading more messages for " + state.appId); 22 | if (MessageState.ALL_MESSAGES == state.appId) { 23 | return Api.execute(messageApi.getMessages(LIMIT, state.nextSince)); 24 | } else { 25 | return Api.execute(messageApi.getAppMessages(state.appId, LIMIT, state.nextSince)); 26 | } 27 | } catch (ApiException apiException) { 28 | Log.e("failed requesting messages", apiException); 29 | return null; 30 | } 31 | } 32 | 33 | void asyncRemoveMessage(Message message) { 34 | Log.i("Removing message with id " + message.getId()); 35 | messageApi.deleteMessage(message.getId()).enqueue(Callback.call()); 36 | } 37 | 38 | boolean deleteAll(Long appId) { 39 | try { 40 | Log.i("Deleting all messages for " + appId); 41 | if (MessageState.ALL_MESSAGES == appId) { 42 | Api.execute(messageApi.deleteMessages()); 43 | } else { 44 | Api.execute(messageApi.deleteAppMessages(appId)); 45 | } 46 | return true; 47 | } catch (ApiException e) { 48 | Log.e("Could not delete messages", e); 49 | return false; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/messages/provider/MessageState.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.messages.provider; 2 | 3 | import com.github.gotify.client.model.Message; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class MessageState { 8 | public static final long ALL_MESSAGES = -1; 9 | 10 | long appId; 11 | boolean loaded; 12 | boolean hasNext; 13 | long nextSince = 0; 14 | List messages = new ArrayList<>(); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/messages/provider/MessageWithImage.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.messages.provider; 2 | 3 | import com.github.gotify.client.model.Message; 4 | 5 | public class MessageWithImage { 6 | public Message message; 7 | public String image; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/picasso/PicassoDataRequestHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.picasso; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.util.Base64; 6 | import com.github.gotify.log.Log; 7 | import com.squareup.picasso.Picasso; 8 | import com.squareup.picasso.Request; 9 | import com.squareup.picasso.RequestHandler; 10 | 11 | /** 12 | * Adapted from https://github.com/square/picasso/issues/1395#issuecomment-220929377 By 13 | * https://github.com/SmartDengg 14 | */ 15 | public class PicassoDataRequestHandler extends RequestHandler { 16 | 17 | private static final String DATA_SCHEME = "data"; 18 | 19 | @Override 20 | public boolean canHandleRequest(Request data) { 21 | String scheme = data.uri.getScheme(); 22 | return DATA_SCHEME.equalsIgnoreCase(scheme); 23 | } 24 | 25 | @Override 26 | public Result load(Request request, int networkPolicy) { 27 | String uri = request.uri.toString(); 28 | String imageDataBytes = uri.substring(uri.indexOf(",") + 1); 29 | byte[] bytes = Base64.decode(imageDataBytes.getBytes(), Base64.DEFAULT); 30 | Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); 31 | 32 | if (bitmap == null) { 33 | String show = uri.length() > 50 ? uri.substring(0, 49) + "..." : uri; 34 | RuntimeException malformed = new RuntimeException("Malformed data uri: " + show); 35 | Log.e("Could not load image", malformed); 36 | throw malformed; 37 | } 38 | 39 | return new Result(bitmap, Picasso.LoadedFrom.NETWORK); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/picasso/PicassoHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.picasso; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import com.github.gotify.R; 7 | import com.github.gotify.Settings; 8 | import com.github.gotify.Utils; 9 | import com.github.gotify.api.Callback; 10 | import com.github.gotify.api.CertUtils; 11 | import com.github.gotify.api.ClientFactory; 12 | import com.github.gotify.client.api.ApplicationApi; 13 | import com.github.gotify.log.Log; 14 | import com.github.gotify.messages.provider.MessageImageCombiner; 15 | import com.squareup.picasso.OkHttp3Downloader; 16 | import com.squareup.picasso.Picasso; 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.util.Map; 20 | import java.util.concurrent.ConcurrentHashMap; 21 | import okhttp3.Cache; 22 | import okhttp3.OkHttpClient; 23 | 24 | public class PicassoHandler { 25 | 26 | private static final int PICASSO_CACHE_SIZE = 50 * 1024 * 1024; // 50 MB 27 | private static final String PICASSO_CACHE_SUBFOLDER = "picasso-cache"; 28 | 29 | private Context context; 30 | private Settings settings; 31 | 32 | private Cache picassoCache; 33 | 34 | private Picasso picasso; 35 | private Map appIdToAppImage = new ConcurrentHashMap<>(); 36 | 37 | public PicassoHandler(Context context, Settings settings) { 38 | this.context = context; 39 | this.settings = settings; 40 | 41 | picassoCache = 42 | new Cache( 43 | new File(context.getCacheDir(), PICASSO_CACHE_SUBFOLDER), 44 | PICASSO_CACHE_SIZE); 45 | picasso = makePicasso(); 46 | } 47 | 48 | private Picasso makePicasso() { 49 | OkHttpClient.Builder builder = new OkHttpClient.Builder(); 50 | builder.cache(picassoCache); 51 | CertUtils.applySslSettings(builder, settings.sslSettings()); 52 | OkHttp3Downloader downloader = new OkHttp3Downloader(builder.build()); 53 | return new Picasso.Builder(context) 54 | .addRequestHandler(new PicassoDataRequestHandler()) 55 | .downloader(downloader) 56 | .build(); 57 | } 58 | 59 | public Bitmap getIcon(Long appId) { 60 | if (appId == -1) { 61 | return BitmapFactory.decodeResource(context.getResources(), R.drawable.gotify); 62 | } 63 | 64 | try { 65 | return picasso.load( 66 | Utils.resolveAbsoluteUrl( 67 | settings.url() + "/", appIdToAppImage.get(appId))) 68 | .get(); 69 | } catch (IOException e) { 70 | Log.e("Could not load image for notification", e); 71 | } 72 | return BitmapFactory.decodeResource(context.getResources(), R.drawable.gotify); 73 | } 74 | 75 | public void updateAppIds() { 76 | ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token()) 77 | .createService(ApplicationApi.class) 78 | .getApps() 79 | .enqueue( 80 | Callback.call( 81 | (apps) -> { 82 | appIdToAppImage.clear(); 83 | appIdToAppImage.putAll(MessageImageCombiner.appIdToImage(apps)); 84 | }, 85 | (t) -> { 86 | appIdToAppImage.clear(); 87 | })); 88 | } 89 | 90 | public Picasso get() { 91 | return picasso; 92 | } 93 | 94 | public void evict() throws IOException { 95 | picassoCache.evictAll(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/service/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.github.gotify.service 2 | 3 | /** 4 | * Constants as defined on the specs 5 | * https://github.com/UnifiedPush/UP-spec/blob/main/specifications.md 6 | */ 7 | 8 | const val ACTION_NEW_ENDPOINT = "org.unifiedpush.android.connector.NEW_ENDPOINT" 9 | const val ACTION_REGISTRATION_FAILED = "org.unifiedpush.android.connector.REGISTRATION_FAILED" 10 | const val ACTION_REGISTRATION_REFUSED = "org.unifiedpush.android.connector.REGISTRATION_REFUSED" 11 | const val ACTION_UNREGISTERED = "org.unifiedpush.android.connector.UNREGISTERED" 12 | const val ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE" 13 | 14 | const val ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER" 15 | const val ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER" 16 | const val ACTION_MESSAGE_ACK = "org.unifiedpush.android.distributor.MESSAGE_ACK" 17 | 18 | const val EXTRA_APPLICATION = "application" 19 | const val EXTRA_TOKEN = "token" 20 | const val EXTRA_ENDPOINT = "endpoint" 21 | const val EXTRA_MESSAGE = "message" 22 | const val EXTRA_MESSAGE_ID = "id" 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/service/PushNotification.kt: -------------------------------------------------------------------------------- 1 | package com.github.gotify.service 2 | 3 | 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | 8 | /** 9 | * These functions are used to send messages to other apps 10 | */ 11 | 12 | fun sendMessage(context: Context, token: String, message: String){ 13 | val application = getApp(context, token) 14 | if (application.isNullOrBlank()) { 15 | return 16 | } 17 | val broadcastIntent = Intent() 18 | broadcastIntent.`package` = application 19 | broadcastIntent.action = ACTION_MESSAGE 20 | broadcastIntent.putExtra(EXTRA_TOKEN, token) 21 | broadcastIntent.putExtra(EXTRA_MESSAGE, message) 22 | context.sendBroadcast(broadcastIntent) 23 | } 24 | 25 | fun sendEndpoint(context: Context, token: String, endpoint: String) { 26 | val application = getApp(context, token) 27 | if (application.isNullOrBlank()) { 28 | return 29 | } 30 | val broadcastIntent = Intent() 31 | broadcastIntent.`package` = application 32 | broadcastIntent.action = ACTION_NEW_ENDPOINT 33 | broadcastIntent.putExtra(EXTRA_TOKEN, token) 34 | broadcastIntent.putExtra(EXTRA_ENDPOINT, endpoint) 35 | context.sendBroadcast(broadcastIntent) 36 | } 37 | 38 | fun sendUnregistered(context: Context, token: String){ 39 | val application = getApp(context, token) 40 | if (application.isNullOrBlank()) { 41 | return 42 | } 43 | val broadcastIntent = Intent() 44 | broadcastIntent.`package` = application 45 | broadcastIntent.action = ACTION_UNREGISTERED 46 | broadcastIntent.putExtra(EXTRA_TOKEN, token) 47 | context.sendBroadcast(broadcastIntent) 48 | } 49 | 50 | fun sendRegistrationFailed(context: Context, application: String, token: String, message: String){ 51 | val broadcastIntent = Intent() 52 | broadcastIntent.`package` = application 53 | broadcastIntent.action = ACTION_REGISTRATION_FAILED 54 | broadcastIntent.putExtra(EXTRA_TOKEN, token) 55 | broadcastIntent.putExtra(EXTRA_MESSAGE, message) 56 | context.sendBroadcast(broadcastIntent) 57 | } 58 | 59 | fun sendRegistrationRefused(context: Context, application: String, token: String, message: String) { 60 | val broadcastIntent = Intent() 61 | broadcastIntent.`package` = application 62 | broadcastIntent.action = ACTION_REGISTRATION_REFUSED 63 | broadcastIntent.putExtra(EXTRA_TOKEN, token) 64 | broadcastIntent.putExtra(EXTRA_MESSAGE, message) 65 | context.sendBroadcast(broadcastIntent) 66 | } 67 | 68 | fun getApp(context: Context, token: String): String?{ 69 | val db = MessagingDatabase(context) 70 | val application = db.getPackageName(token) 71 | db.close() 72 | if (application.isBlank()) { 73 | Log.w("getApp", "No app found for $token") 74 | return null 75 | } 76 | return application 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/service/RegisterBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.github.gotify.service 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | import com.github.gotify.Settings 8 | import com.github.gotify.api.Api 9 | import com.github.gotify.api.ApiException 10 | import com.github.gotify.api.ClientFactory 11 | import com.github.gotify.client.api.ApplicationApi 12 | import java.text.SimpleDateFormat 13 | import java.util.Date 14 | import kotlin.concurrent.thread 15 | 16 | /** 17 | * THIS SERVICE IS USED BY OTHER APPS TO REGISTER 18 | */ 19 | 20 | class RegisterBroadcastReceiver: BroadcastReceiver() { 21 | 22 | private lateinit var settings: Settings 23 | 24 | private fun registerApp(context: Context?, db: MessagingDatabase, application: String, connectorToken: String) { 25 | if (application.isBlank()) { 26 | Log.w("RegisterService","Trying to register an app without packageName") 27 | return 28 | } 29 | Log.i("RegisterService","registering $application token: $connectorToken") 30 | // The app is registered with the same token : we re-register it 31 | // the client may need its endpoint again 32 | if (db.isRegistered(connectorToken)) { 33 | Log.i("RegisterService","$application already registered") 34 | return 35 | } 36 | 37 | val app = createApp(application) 38 | if(app == null){ 39 | val message = "Cannot create a new app to register" 40 | Log.w("RegisterService", message) 41 | sendRegistrationFailed(context!!,application,connectorToken,message) 42 | return 43 | } 44 | db.registerApp(application, app.id, app.token, connectorToken) 45 | } 46 | 47 | private fun createApp(appName: String): com.github.gotify.client.model.Application? { 48 | val client = ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token()) 49 | val app = com.github.gotify.client.model.Application() 50 | app.name = appName 51 | val date = SimpleDateFormat("dd-MM-yyyy HH:mm:ss").format(Date()) 52 | app.description = "(auto) $date" 53 | try { 54 | Log.i("RegisterService","Creating app") 55 | return Api.execute(client.createService(ApplicationApi::class.java).createApp(app)) 56 | } catch (e: ApiException) { 57 | Log.e("RegisterService","Could not create app.", e) 58 | } 59 | return null 60 | } 61 | 62 | private fun deleteApp(db: MessagingDatabase, token: String) { 63 | val client = ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token()) 64 | try { 65 | val appId = db.getAppId(token) 66 | Log.i("RegisterService","Deleting app with appId=$appId") 67 | Api.execute(client.createService(ApplicationApi::class.java).deleteApp(appId)) 68 | db.unregisterApp(token) 69 | } catch (e: ApiException) { 70 | Log.e("RegisterService","Could not delete app.", e) 71 | } 72 | } 73 | 74 | override fun onReceive(context: Context?, intent: Intent?) { 75 | settings = Settings(context) 76 | when (intent!!.action) { 77 | ACTION_REGISTER ->{ 78 | val db = MessagingDatabase(context!!) 79 | Log.i("Register","REGISTER") 80 | val connectorToken = intent.getStringExtra(EXTRA_TOKEN)?: "" 81 | val application = intent.getStringExtra(EXTRA_APPLICATION)?: "" 82 | thread(start = true) { 83 | registerApp(context, db, application, connectorToken) 84 | Log.i("RegisterService","Registration is finished") 85 | }.join() 86 | val token = db.getGotifyToken(connectorToken) 87 | db.close() 88 | val endpoint = settings.url() + 89 | "/UP?token=$token" 90 | sendEndpoint(context, connectorToken, endpoint) 91 | } 92 | ACTION_UNREGISTER ->{ 93 | Log.i("Register","UNREGISTER") 94 | val token = intent.getStringExtra(EXTRA_TOKEN)?: "" 95 | thread(start = true) { 96 | val db = MessagingDatabase(context!!) 97 | deleteApp(db, token) 98 | db.close() 99 | Log.i("RegisterService","Unregistration is finished") 100 | } 101 | sendUnregistered(context!!, token) 102 | } 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/settings/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.settings; 2 | 3 | import android.content.SharedPreferences; 4 | import android.os.Bundle; 5 | import android.view.MenuItem; 6 | import androidx.annotation.NonNull; 7 | import androidx.appcompat.app.ActionBar; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import androidx.preference.PreferenceFragmentCompat; 10 | import androidx.preference.PreferenceManager; 11 | import com.github.gotify.R; 12 | 13 | public class SettingsActivity extends AppCompatActivity 14 | implements SharedPreferences.OnSharedPreferenceChangeListener { 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.settings_activity); 20 | getSupportFragmentManager() 21 | .beginTransaction() 22 | .replace(R.id.settings, new SettingsFragment()) 23 | .commit(); 24 | setSupportActionBar(findViewById(R.id.toolbar)); 25 | ActionBar actionBar = getSupportActionBar(); 26 | if (actionBar != null) { 27 | actionBar.setDisplayHomeAsUpEnabled(true); 28 | actionBar.setDisplayShowCustomEnabled(true); 29 | } 30 | SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 31 | sharedPreferences.registerOnSharedPreferenceChangeListener(this); 32 | } 33 | 34 | @Override 35 | public boolean onOptionsItemSelected(@NonNull MenuItem item) { 36 | if (item.getItemId() == android.R.id.home) { 37 | finish(); 38 | } 39 | return super.onOptionsItemSelected(item); 40 | } 41 | 42 | @Override 43 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 44 | if (getString(R.string.setting_key_theme).equals(key)) { 45 | ThemeHelper.setTheme( 46 | this, sharedPreferences.getString(key, getString(R.string.theme_default))); 47 | } 48 | } 49 | 50 | public static class SettingsFragment extends PreferenceFragmentCompat { 51 | @Override 52 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 53 | setPreferencesFromResource(R.xml.root_preferences, rootKey); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/gotify/settings/ThemeHelper.java: -------------------------------------------------------------------------------- 1 | package com.github.gotify.settings; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import androidx.appcompat.app.AppCompatDelegate; 6 | import com.github.gotify.R; 7 | 8 | public final class ThemeHelper { 9 | private ThemeHelper() {} 10 | 11 | public static void setTheme(Context context, String newTheme) { 12 | AppCompatDelegate.setDefaultNightMode(ofKey(context, newTheme)); 13 | } 14 | 15 | private static int ofKey(Context context, String newTheme) { 16 | if (context.getString(R.string.theme_dark).equals(newTheme)) { 17 | return AppCompatDelegate.MODE_NIGHT_YES; 18 | } 19 | if (context.getString(R.string.theme_light).equals(newTheme)) { 20 | return AppCompatDelegate.MODE_NIGHT_NO; 21 | } 22 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { 23 | return AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY; 24 | } 25 | return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_gotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnifiedPush/gotify-android/567b4d3b8c1b02a9cab03a4348683936e775d1dd/app/src/main/res/drawable-hdpi/ic_gotify.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_gotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnifiedPush/gotify-android/567b4d3b8c1b02a9cab03a4348683936e775d1dd/app/src/main/res/drawable-mdpi/ic_gotify.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_gotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnifiedPush/gotify-android/567b4d3b8c1b02a9cab03a4348683936e775d1dd/app/src/main/res/drawable-xhdpi/ic_gotify.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_gotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnifiedPush/gotify-android/567b4d3b8c1b02a9cab03a4348683936e775d1dd/app/src/main/res/drawable-xxhdpi/ic_gotify.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_gotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnifiedPush/gotify-android/567b4d3b8c1b02a9cab03a4348683936e775d1dd/app/src/main/res/drawable-xxxhdpi/ic_gotify.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/gotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnifiedPush/gotify-android/567b4d3b8c1b02a9cab03a4348683936e775d1dd/app/src/main/res/drawable/gotify.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_alarm.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bug_report.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dashboard.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_placeholder.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_power_setting.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_send.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_logs.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 19 | 20 | 36 | 37 | 38 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_messages.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 20 | 21 | 27 | 28 | 32 | 33 | 39 | 40 | 41 | 47 | 48 | 62 | 63 |