├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ ├── feature-request.md │ └── question.md └── workflows │ └── build.yml ├── .gitignore ├── COPYING ├── Gemfile ├── Gemfile.lock ├── README.md ├── app ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── dev │ │ └── patri9ck │ │ └── a2ln │ │ ├── app │ │ ├── App.java │ │ ├── AppsAdapter.java │ │ └── AppsFragment.java │ │ ├── log │ │ ├── KeptLog.java │ │ └── LogDialogBuilder.java │ │ ├── main │ │ ├── Application.java │ │ └── MainActivity.java │ │ ├── notification │ │ ├── NotificationReceiver.java │ │ ├── NotificationSender.java │ │ ├── ParsedNotification.java │ │ └── spam │ │ │ ├── NotificationSpamHandler.java │ │ │ └── StrippedNotification.java │ │ ├── pairing │ │ ├── Pairing.java │ │ └── PairingResult.java │ │ ├── server │ │ ├── Destination.java │ │ ├── DragAndDropCallback.java │ │ ├── Server.java │ │ ├── ServersAdapter.java │ │ ├── ServersFragment.java │ │ └── SwipeToDeleteCallback.java │ │ ├── settings │ │ └── SettingsFragment.java │ │ └── util │ │ ├── Storage.java │ │ └── Util.java │ └── res │ ├── drawable │ ├── ic_apps.xml │ ├── ic_help.xml │ ├── ic_notification.xml │ ├── ic_pair.xml │ ├── ic_permission.xml │ ├── ic_qr_code.xml │ ├── ic_servers.xml │ └── ic_settings.xml │ ├── layout │ ├── activity_main.xml │ ├── dialog_edit_server.xml │ ├── dialog_pair.xml │ ├── dialog_paired.xml │ ├── dialog_pairing.xml │ ├── fragment_apps.xml │ ├── fragment_servers.xml │ ├── fragment_settings.xml │ ├── item_app.xml │ └── item_server.xml │ ├── menu │ └── menu_bottom_navigation.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── navigation │ └── navigation_mobile.xml │ ├── values-de-rDE │ ├── logs.xml │ └── strings.xml │ ├── values-night │ ├── colors.xml │ └── themes.xml │ ├── values-ru │ ├── logs.xml │ └── strings.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── logs.xml │ ├── preferences.xml │ ├── static.xml │ ├── strings.xml │ ├── styles.xml │ └── themes.xml ├── build.gradle.kts ├── fastlane ├── Appfile ├── Fastfile └── metadata │ └── android │ ├── de-DE │ ├── changelogs │ │ ├── 15.txt │ │ ├── 16.txt │ │ ├── 17.txt │ │ ├── 18.txt │ │ ├── 19.txt │ │ └── 20.txt │ ├── full_description.txt │ ├── images │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ ├── short_description.txt │ └── title.txt │ ├── en-US │ ├── changelogs │ │ ├── 1.txt │ │ ├── 10.txt │ │ ├── 11.txt │ │ ├── 12.txt │ │ ├── 13.txt │ │ ├── 14.txt │ │ ├── 15.txt │ │ ├── 16.txt │ │ ├── 17.txt │ │ ├── 18.txt │ │ ├── 19.txt │ │ ├── 2.txt │ │ ├── 20.txt │ │ ├── 3.txt │ │ ├── 4.txt │ │ ├── 5.txt │ │ ├── 6.txt │ │ ├── 7.txt │ │ ├── 8.txt │ │ └── 9.txt │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.png │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ ├── short_description.txt │ └── title.txt │ └── ru │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── banner.png ├── icon-filled.png └── icon-transparent.png └── settings.gradle.kts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: patri9ck 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Description 11 | A clear and concise description of what the bug is. 12 | 13 | # Reproduction 14 | Steps to reproduce the behavior. 15 | 16 | # Screenshots 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | # Smartphone 20 | - Device: 21 | - Android version: 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Installation Help 4 | url: https://patri9ck.dev/a2ln/app.html 5 | about: Everything you will need to install the app 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Request a feature 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Is your feature request related to a problem? 11 | A clear and concise description of what the problem is. 12 | 13 | # Description 14 | A clear and concise description of what you want to happen. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Ask your question, whatever it may be 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build APK 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - name: Set up JDK 14 | uses: actions/setup-java@v3 15 | with: 16 | distribution: 'temurin' 17 | java-version: '17' 18 | cache: 'gradle' 19 | - name: Build APK 20 | run: ./gradlew clean assembleDebug 21 | - name: Upload artifact 22 | uses: actions/upload-artifact@v3 23 | with: 24 | name: apk 25 | path: app/build/outputs/apk/debug/app-debug.apk 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .gradle 3 | local.properties 4 | app/build/ 5 | keystore.properties 6 | fastlane/README.md 7 | fastlane/report.xml 8 | fastlane/metadata/android/**/screenshots.html 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | addressable (2.8.0) 7 | public_suffix (>= 2.0.2, < 5.0) 8 | artifactory (3.0.15) 9 | atomos (0.1.3) 10 | aws-eventstream (1.2.0) 11 | aws-partitions (1.602.0) 12 | aws-sdk-core (3.131.2) 13 | aws-eventstream (~> 1, >= 1.0.2) 14 | aws-partitions (~> 1, >= 1.525.0) 15 | aws-sigv4 (~> 1.1) 16 | jmespath (~> 1, >= 1.6.1) 17 | aws-sdk-kms (1.57.0) 18 | aws-sdk-core (~> 3, >= 3.127.0) 19 | aws-sigv4 (~> 1.1) 20 | aws-sdk-s3 (1.114.0) 21 | aws-sdk-core (~> 3, >= 3.127.0) 22 | aws-sdk-kms (~> 1) 23 | aws-sigv4 (~> 1.4) 24 | aws-sigv4 (1.5.0) 25 | aws-eventstream (~> 1, >= 1.0.2) 26 | babosa (1.0.4) 27 | claide (1.1.0) 28 | colored (1.2) 29 | colored2 (3.1.2) 30 | commander (4.6.0) 31 | highline (~> 2.0.0) 32 | declarative (0.0.20) 33 | digest-crc (0.6.4) 34 | rake (>= 12.0.0, < 14.0.0) 35 | domain_name (0.5.20190701) 36 | unf (>= 0.0.5, < 1.0.0) 37 | dotenv (2.7.6) 38 | emoji_regex (3.2.3) 39 | excon (0.92.3) 40 | faraday (1.10.0) 41 | faraday-em_http (~> 1.0) 42 | faraday-em_synchrony (~> 1.0) 43 | faraday-excon (~> 1.1) 44 | faraday-httpclient (~> 1.0) 45 | faraday-multipart (~> 1.0) 46 | faraday-net_http (~> 1.0) 47 | faraday-net_http_persistent (~> 1.0) 48 | faraday-patron (~> 1.0) 49 | faraday-rack (~> 1.0) 50 | faraday-retry (~> 1.0) 51 | ruby2_keywords (>= 0.0.4) 52 | faraday-cookie_jar (0.0.7) 53 | faraday (>= 0.8.0) 54 | http-cookie (~> 1.0.0) 55 | faraday-em_http (1.0.0) 56 | faraday-em_synchrony (1.0.0) 57 | faraday-excon (1.1.0) 58 | faraday-httpclient (1.0.1) 59 | faraday-multipart (1.0.4) 60 | multipart-post (~> 2) 61 | faraday-net_http (1.0.1) 62 | faraday-net_http_persistent (1.2.0) 63 | faraday-patron (1.0.0) 64 | faraday-rack (1.0.0) 65 | faraday-retry (1.0.3) 66 | faraday_middleware (1.2.0) 67 | faraday (~> 1.0) 68 | fastimage (2.2.6) 69 | fastlane (2.207.0) 70 | CFPropertyList (>= 2.3, < 4.0.0) 71 | addressable (>= 2.8, < 3.0.0) 72 | artifactory (~> 3.0) 73 | aws-sdk-s3 (~> 1.0) 74 | babosa (>= 1.0.3, < 2.0.0) 75 | bundler (>= 1.12.0, < 3.0.0) 76 | colored 77 | commander (~> 4.6) 78 | dotenv (>= 2.1.1, < 3.0.0) 79 | emoji_regex (>= 0.1, < 4.0) 80 | excon (>= 0.71.0, < 1.0.0) 81 | faraday (~> 1.0) 82 | faraday-cookie_jar (~> 0.0.6) 83 | faraday_middleware (~> 1.0) 84 | fastimage (>= 2.1.0, < 3.0.0) 85 | gh_inspector (>= 1.1.2, < 2.0.0) 86 | google-apis-androidpublisher_v3 (~> 0.3) 87 | google-apis-playcustomapp_v1 (~> 0.1) 88 | google-cloud-storage (~> 1.31) 89 | highline (~> 2.0) 90 | json (< 3.0.0) 91 | jwt (>= 2.1.0, < 3) 92 | mini_magick (>= 4.9.4, < 5.0.0) 93 | multipart-post (~> 2.0.0) 94 | naturally (~> 2.2) 95 | optparse (~> 0.1.1) 96 | plist (>= 3.1.0, < 4.0.0) 97 | rubyzip (>= 2.0.0, < 3.0.0) 98 | security (= 0.1.3) 99 | simctl (~> 1.6.3) 100 | terminal-notifier (>= 2.0.0, < 3.0.0) 101 | terminal-table (>= 1.4.5, < 2.0.0) 102 | tty-screen (>= 0.6.3, < 1.0.0) 103 | tty-spinner (>= 0.8.0, < 1.0.0) 104 | word_wrap (~> 1.0.0) 105 | xcodeproj (>= 1.13.0, < 2.0.0) 106 | xcpretty (~> 0.3.0) 107 | xcpretty-travis-formatter (>= 0.0.3) 108 | gh_inspector (1.1.3) 109 | google-apis-androidpublisher_v3 (0.23.0) 110 | google-apis-core (>= 0.6, < 2.a) 111 | google-apis-core (0.7.0) 112 | addressable (~> 2.5, >= 2.5.1) 113 | googleauth (>= 0.16.2, < 2.a) 114 | httpclient (>= 2.8.1, < 3.a) 115 | mini_mime (~> 1.0) 116 | representable (~> 3.0) 117 | retriable (>= 2.0, < 4.a) 118 | rexml 119 | webrick 120 | google-apis-iamcredentials_v1 (0.12.0) 121 | google-apis-core (>= 0.6, < 2.a) 122 | google-apis-playcustomapp_v1 (0.9.0) 123 | google-apis-core (>= 0.6, < 2.a) 124 | google-apis-storage_v1 (0.16.0) 125 | google-apis-core (>= 0.6, < 2.a) 126 | google-cloud-core (1.6.0) 127 | google-cloud-env (~> 1.0) 128 | google-cloud-errors (~> 1.0) 129 | google-cloud-env (1.6.0) 130 | faraday (>= 0.17.3, < 3.0) 131 | google-cloud-errors (1.2.0) 132 | google-cloud-storage (1.37.0) 133 | addressable (~> 2.8) 134 | digest-crc (~> 0.4) 135 | google-apis-iamcredentials_v1 (~> 0.1) 136 | google-apis-storage_v1 (~> 0.1) 137 | google-cloud-core (~> 1.6) 138 | googleauth (>= 0.16.2, < 2.a) 139 | mini_mime (~> 1.0) 140 | googleauth (1.2.0) 141 | faraday (>= 0.17.3, < 3.a) 142 | jwt (>= 1.4, < 3.0) 143 | memoist (~> 0.16) 144 | multi_json (~> 1.11) 145 | os (>= 0.9, < 2.0) 146 | signet (>= 0.16, < 2.a) 147 | highline (2.0.3) 148 | http-cookie (1.0.5) 149 | domain_name (~> 0.5) 150 | httpclient (2.8.3) 151 | jmespath (1.6.1) 152 | json (2.6.2) 153 | jwt (2.4.1) 154 | memoist (0.16.2) 155 | mini_magick (4.11.0) 156 | mini_mime (1.1.2) 157 | multi_json (1.15.0) 158 | multipart-post (2.0.0) 159 | nanaimo (0.3.0) 160 | naturally (2.2.1) 161 | optparse (0.1.1) 162 | os (1.1.4) 163 | plist (3.6.0) 164 | public_suffix (4.0.7) 165 | rake (13.0.6) 166 | representable (3.2.0) 167 | declarative (< 0.1.0) 168 | trailblazer-option (>= 0.1.1, < 0.2.0) 169 | uber (< 0.2.0) 170 | retriable (3.1.2) 171 | rexml (3.2.5) 172 | rouge (2.0.7) 173 | ruby2_keywords (0.0.5) 174 | rubyzip (2.3.2) 175 | security (0.1.3) 176 | signet (0.17.0) 177 | addressable (~> 2.8) 178 | faraday (>= 0.17.5, < 3.a) 179 | jwt (>= 1.5, < 3.0) 180 | multi_json (~> 1.10) 181 | simctl (1.6.8) 182 | CFPropertyList 183 | naturally 184 | terminal-notifier (2.0.0) 185 | terminal-table (1.8.0) 186 | unicode-display_width (~> 1.1, >= 1.1.1) 187 | trailblazer-option (0.1.2) 188 | tty-cursor (0.7.1) 189 | tty-screen (0.8.1) 190 | tty-spinner (0.9.3) 191 | tty-cursor (~> 0.7) 192 | uber (0.1.0) 193 | unf (0.1.4) 194 | unf_ext 195 | unf_ext (0.0.8.2) 196 | unicode-display_width (1.8.0) 197 | webrick (1.7.0) 198 | word_wrap (1.0.0) 199 | xcodeproj (1.22.0) 200 | CFPropertyList (>= 2.3.3, < 4.0) 201 | atomos (~> 0.1.3) 202 | claide (>= 1.0.2, < 2.0) 203 | colored2 (~> 3.1) 204 | nanaimo (~> 0.3.0) 205 | rexml (~> 3.2.4) 206 | xcpretty (0.3.0) 207 | rouge (~> 2.0.7) 208 | xcpretty-travis-formatter (1.0.1) 209 | xcpretty (~> 0.2, >= 0.0.7) 210 | 211 | PLATFORMS 212 | x86_64-linux 213 | 214 | DEPENDENCIES 215 | fastlane 216 | 217 | BUNDLED WITH 218 | 2.3.17 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android 2 Linux Notifications App 2 | **Android 2 Linux Notifications** (**A2LN**) is a way to display your Android phone notifications on your Linux computer. This repository contains the app part of A2LN. 3 | 4 | More information can be found at [patri9ck.dev/a2ln](https://patri9ck.dev/a2ln/). 5 | 6 | ## License 7 | A2LN is licensed under the [GNU General Public License Version 3 or later](COPYING). 8 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | import java.io.FileInputStream 19 | import java.util.Properties 20 | 21 | plugins { 22 | id("com.android.application") 23 | } 24 | 25 | android { 26 | namespace = "dev.patri9ck.a2ln" 27 | 28 | compileSdk = 34 29 | 30 | defaultConfig { 31 | applicationId = "dev.patri9ck.a2ln" 32 | minSdk = 27 33 | targetSdk = 34 34 | 35 | versionCode = 20 36 | versionName = "1.4.0" 37 | } 38 | 39 | compileOptions { 40 | sourceCompatibility(JavaVersion.VERSION_17) 41 | targetCompatibility(JavaVersion.VERSION_17) 42 | } 43 | 44 | buildFeatures { 45 | viewBinding = true 46 | buildConfig = true 47 | } 48 | 49 | val keystoreFile = rootProject.file("keystore.properties") 50 | 51 | if (keystoreFile.exists()) { 52 | val keystoreProperties = Properties() 53 | 54 | keystoreProperties.load(FileInputStream(keystoreFile)) 55 | 56 | signingConfigs { 57 | create("release") { 58 | storeFile = file(keystoreProperties.getProperty("storeFile")) 59 | storePassword = keystoreProperties.getProperty("storePassword") 60 | 61 | keyAlias = keystoreProperties.getProperty("keyAlias") 62 | keyPassword = keystoreProperties.getProperty("keyPassword") 63 | } 64 | } 65 | 66 | buildTypes { 67 | getByName("release") { 68 | signingConfig = signingConfigs.getByName("release") 69 | } 70 | } 71 | } 72 | } 73 | 74 | dependencies { 75 | implementation("com.google.code.gson:gson:2.10") 76 | implementation("com.google.android.material:material:1.11.0") 77 | implementation("org.zeromq:jeromq:0.5.2") 78 | implementation("androidx.appcompat:appcompat:1.6.1") 79 | implementation("androidx.navigation:navigation-fragment:2.7.7") 80 | implementation("androidx.navigation:navigation-ui:2.7.7") 81 | implementation("com.journeyapps:zxing-android-embedded:4.3.0") 82 | implementation("me.xdrop:fuzzywuzzy:1.4.0") 83 | implementation("net.jodah:expiringmap:0.5.11") 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 18 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 37 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patri9ck/a2ln-app/2c37b25f1adde602fb30a42b86bc7a4e23672876/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/app/App.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.app; 19 | 20 | import android.graphics.drawable.Drawable; 21 | 22 | public class App { 23 | 24 | private final String name; 25 | private final String packageName; 26 | private final Drawable icon; 27 | private boolean enabled; 28 | 29 | public App(String name, String packageName, Drawable icon, boolean enabled) { 30 | this.name = name; 31 | this.packageName = packageName; 32 | this.icon = icon; 33 | this.enabled = enabled; 34 | } 35 | 36 | public String getName() { 37 | return name; 38 | } 39 | 40 | public String getPackageName() { 41 | return packageName; 42 | } 43 | 44 | public Drawable getIcon() { 45 | return icon; 46 | } 47 | 48 | public boolean isEnabled() { 49 | return enabled; 50 | } 51 | 52 | public void setEnabled(boolean enabled) { 53 | this.enabled = enabled; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/app/AppsAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.app; 19 | 20 | import android.view.LayoutInflater; 21 | import android.view.ViewGroup; 22 | import android.widget.CheckBox; 23 | import android.widget.ImageView; 24 | import android.widget.TextView; 25 | 26 | import androidx.annotation.NonNull; 27 | import androidx.recyclerview.widget.RecyclerView; 28 | 29 | import java.util.List; 30 | 31 | import dev.patri9ck.a2ln.databinding.ItemAppBinding; 32 | import dev.patri9ck.a2ln.util.Storage; 33 | 34 | public class AppsAdapter extends RecyclerView.Adapter { 35 | 36 | private final List disabledApps; 37 | private final Storage storage; 38 | private final List apps; 39 | 40 | public AppsAdapter(List disabledApps, List apps, Storage storage) { 41 | this.disabledApps = disabledApps; 42 | this.apps = apps; 43 | this.storage = storage; 44 | } 45 | 46 | @NonNull 47 | @Override 48 | public AppViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 49 | return new AppViewHolder(ItemAppBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); 50 | } 51 | 52 | @Override 53 | public void onBindViewHolder(AppViewHolder holder, int position) { 54 | App app = apps.get(position); 55 | 56 | holder.appCheckBox.setOnCheckedChangeListener((appCheckBoxView, isChecked) -> { 57 | app.setEnabled(isChecked); 58 | 59 | String packageName = app.getPackageName(); 60 | 61 | if (isChecked) { 62 | disabledApps.remove(packageName); 63 | } else if (!disabledApps.contains(packageName)) { 64 | disabledApps.add(packageName); 65 | } 66 | 67 | storage.saveDisabledApps(disabledApps); 68 | }); 69 | 70 | holder.appCheckBox.setChecked(app.isEnabled()); 71 | holder.nameTextView.setText(app.getName()); 72 | holder.iconImageView.setImageDrawable(app.getIcon()); 73 | } 74 | 75 | @Override 76 | public int getItemCount() { 77 | return apps.size(); 78 | } 79 | 80 | protected static class AppViewHolder extends RecyclerView.ViewHolder { 81 | 82 | private final TextView nameTextView; 83 | private final CheckBox appCheckBox; 84 | private final ImageView iconImageView; 85 | 86 | public AppViewHolder(ItemAppBinding itemAppBinding) { 87 | super(itemAppBinding.getRoot()); 88 | 89 | nameTextView = itemAppBinding.nameTextView; 90 | appCheckBox = itemAppBinding.appCheckBox; 91 | iconImageView = itemAppBinding.iconImageView; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/app/AppsFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.app; 19 | 20 | import android.content.Context; 21 | import android.content.pm.PackageManager; 22 | import android.os.Bundle; 23 | import android.view.LayoutInflater; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | 27 | import androidx.annotation.NonNull; 28 | import androidx.fragment.app.Fragment; 29 | import androidx.recyclerview.widget.LinearLayoutManager; 30 | 31 | import java.util.Comparator; 32 | import java.util.List; 33 | import java.util.concurrent.CompletableFuture; 34 | import java.util.stream.Collectors; 35 | 36 | import dev.patri9ck.a2ln.R; 37 | import dev.patri9ck.a2ln.databinding.FragmentAppsBinding; 38 | import dev.patri9ck.a2ln.util.Storage; 39 | 40 | public class AppsFragment extends Fragment { 41 | 42 | private Storage storage; 43 | private List disabledApps; 44 | private FragmentAppsBinding fragmentAppsBinding; 45 | 46 | @Override 47 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 48 | storage = new Storage(requireContext(), requireContext().getSharedPreferences(getString(R.string.preferences), Context.MODE_PRIVATE)); 49 | disabledApps = storage.loadDisabledApps(); 50 | fragmentAppsBinding = FragmentAppsBinding.inflate(inflater, container, false); 51 | 52 | loadAppsRecyclerView(); 53 | 54 | return fragmentAppsBinding.getRoot(); 55 | } 56 | 57 | @Override 58 | public void onDestroyView() { 59 | super.onDestroyView(); 60 | 61 | fragmentAppsBinding = null; 62 | } 63 | 64 | private void loadAppsRecyclerView() { 65 | fragmentAppsBinding.loadingProgressIndicator.setVisibility(View.VISIBLE); 66 | 67 | PackageManager packageManager = requireContext().getPackageManager(); 68 | 69 | CompletableFuture.supplyAsync(() -> packageManager.getInstalledApplications(PackageManager.GET_META_DATA) 70 | .stream() 71 | .filter(applicationInfo -> packageManager.getLaunchIntentForPackage(applicationInfo.packageName) != null) 72 | .map(applicationInfo -> new App(applicationInfo.loadLabel(packageManager).toString(), applicationInfo.packageName, applicationInfo.loadIcon(packageManager), !disabledApps.contains(applicationInfo.packageName))) 73 | .sorted(Comparator.comparing(App::isEnabled).thenComparing(App::getName)) 74 | .collect(Collectors.toList())) 75 | .thenAccept(apps -> requireActivity().runOnUiThread(() -> { 76 | fragmentAppsBinding.loadingProgressIndicator.setVisibility(View.INVISIBLE); 77 | 78 | fragmentAppsBinding.appsRecyclerView.setAdapter(new AppsAdapter(disabledApps, apps, storage)); 79 | fragmentAppsBinding.appsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); 80 | })); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/log/KeptLog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.log; 19 | 20 | import android.content.Context; 21 | 22 | import java.text.DateFormat; 23 | import java.util.ArrayList; 24 | import java.util.Date; 25 | import java.util.List; 26 | 27 | public class KeptLog { 28 | 29 | private final List messages = new ArrayList<>(); 30 | 31 | private final Context context; 32 | 33 | public KeptLog(Context context) { 34 | this.context = context; 35 | } 36 | 37 | public void log(int id, Object... arguments) { 38 | String message = context.getString(id, arguments); 39 | 40 | messages.add(DateFormat.getTimeInstance().format(new Date()) + ": " + message); 41 | } 42 | 43 | public String format() { 44 | return String.join("\n", messages); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/log/LogDialogBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.log; 19 | 20 | import android.view.LayoutInflater; 21 | 22 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 23 | 24 | import dev.patri9ck.a2ln.R; 25 | 26 | public class LogDialogBuilder extends MaterialAlertDialogBuilder { 27 | 28 | public LogDialogBuilder(String log, LayoutInflater layoutInflater) { 29 | super(layoutInflater.getContext(), R.style.Dialog); 30 | 31 | setTitle(R.string.log_dialog_title); 32 | setMessage(log); 33 | setNegativeButton(R.string.cancel, null); 34 | } 35 | 36 | public LogDialogBuilder(KeptLog keptLog, LayoutInflater layoutInflater) { 37 | this(keptLog.format(), layoutInflater); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/main/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2024 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.main; 19 | 20 | import com.google.android.material.color.DynamicColors; 21 | 22 | public class Application extends android.app.Application { 23 | @Override 24 | public void onCreate() { 25 | super.onCreate(); 26 | DynamicColors.applyToActivitiesIfAvailable(this); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/main/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.main; 19 | 20 | import android.app.NotificationChannel; 21 | import android.app.NotificationManager; 22 | import android.content.Context; 23 | import android.content.Intent; 24 | import android.content.SharedPreferences; 25 | import android.os.Bundle; 26 | import android.provider.Settings; 27 | 28 | import androidx.appcompat.app.AppCompatActivity; 29 | import androidx.core.app.NotificationManagerCompat; 30 | import androidx.core.view.WindowCompat; 31 | import androidx.navigation.NavController; 32 | import androidx.navigation.fragment.NavHostFragment; 33 | import androidx.navigation.ui.AppBarConfiguration; 34 | import androidx.navigation.ui.NavigationUI; 35 | 36 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 37 | 38 | import org.zeromq.ZCert; 39 | 40 | import dev.patri9ck.a2ln.R; 41 | import dev.patri9ck.a2ln.databinding.ActivityMainBinding; 42 | 43 | public class MainActivity extends AppCompatActivity { 44 | 45 | private ActivityMainBinding activityMainBinding; 46 | 47 | @Override 48 | protected void onCreate(Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | 51 | activityMainBinding = ActivityMainBinding.inflate(getLayoutInflater()); 52 | 53 | setSupportActionBar(activityMainBinding.topAppBar); 54 | setContentView(activityMainBinding.getRoot()); 55 | 56 | generateKeys(); 57 | loadNavigationBar(); 58 | requestPermission(); 59 | createNotificationChannel(); 60 | 61 | WindowCompat.setDecorFitsSystemWindows(getWindow(), false); 62 | } 63 | 64 | private void generateKeys() { 65 | SharedPreferences sharedPreferences = getSharedPreferences(getString(R.string.preferences), Context.MODE_PRIVATE); 66 | 67 | if (sharedPreferences.contains(getString(R.string.preferences_public_key)) && sharedPreferences.contains(getString(R.string.preferences_secret_key))) { 68 | return; 69 | } 70 | 71 | ZCert zCert = new ZCert(); 72 | 73 | sharedPreferences.edit() 74 | .putString(getString(R.string.preferences_public_key), zCert.getPublicKeyAsZ85()) 75 | .putString(getString(R.string.preferences_secret_key), zCert.getSecretKeyAsZ85()) 76 | .apply(); 77 | } 78 | 79 | private void loadNavigationBar() { 80 | NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.main_fragment_container_view); 81 | 82 | if (navHostFragment == null) { 83 | return; 84 | } 85 | 86 | NavController navController = navHostFragment.getNavController(); 87 | 88 | NavigationUI.setupActionBarWithNavController(this, navController, new AppBarConfiguration.Builder(R.id.navigation_servers, R.id.navigation_apps, R.id.navigation_settings) 89 | .build()); 90 | NavigationUI.setupWithNavController(activityMainBinding.mainBottomNavigationView, navController); 91 | } 92 | 93 | private void requestPermission() { 94 | if (NotificationManagerCompat.getEnabledListenerPackages(this).contains(getPackageName())) { 95 | return; 96 | } 97 | new MaterialAlertDialogBuilder(this, R.style.Dialog) 98 | .setTitle(R.string.permission_request_dialog_title) 99 | .setMessage(R.string.permission_request_dialog_listener_information) 100 | .setPositiveButton(R.string.grant, (requestPermissionDialog, which) -> startActivity(new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))) 101 | .setNegativeButton(R.string.cancel, null) 102 | .show(); 103 | } 104 | 105 | private void createNotificationChannel() { 106 | NotificationChannel notificationChannel = new NotificationChannel(getString(R.string.channel_id), getString(R.string.channel_name), NotificationManager.IMPORTANCE_NONE); 107 | 108 | notificationChannel.setDescription(getString(R.string.channel_description)); 109 | 110 | NotificationManagerCompat.from(this).createNotificationChannel(notificationChannel); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/notification/NotificationReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.notification; 19 | 20 | import android.content.Context; 21 | import android.content.SharedPreferences; 22 | import android.content.pm.PackageManager; 23 | import android.hardware.display.DisplayManager; 24 | import android.service.notification.NotificationListenerService; 25 | import android.service.notification.StatusBarNotification; 26 | import android.util.Log; 27 | import android.view.Display; 28 | 29 | import java.util.List; 30 | import java.util.Optional; 31 | import java.util.concurrent.CompletableFuture; 32 | 33 | import dev.patri9ck.a2ln.R; 34 | import dev.patri9ck.a2ln.notification.spam.NotificationSpamHandler; 35 | import dev.patri9ck.a2ln.util.Storage; 36 | 37 | public class NotificationReceiver extends NotificationListenerService { 38 | 39 | private static final String TAG = "A2LNNR"; 40 | 41 | private boolean initialized; 42 | 43 | private SharedPreferences sharedPreferences; 44 | private Storage storage; 45 | 46 | private NotificationSender notificationSender; 47 | 48 | private NotificationSpamHandler notificationSpamHandler; 49 | 50 | private List disabledApps; 51 | 52 | private boolean display; 53 | private boolean noApp; 54 | 55 | private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, key) -> { 56 | if (getString(R.string.preferences_servers).equals(key)) { 57 | notificationSender.setServers(storage.loadServers()); 58 | 59 | return; 60 | } 61 | 62 | if (getString(R.string.preferences_similarity).equals(key)) { 63 | notificationSpamHandler.setSimilarity(storage.loadSimilarityOrDefault()); 64 | 65 | return; 66 | } 67 | 68 | if (getString(R.string.preferences_duration).equals(key)) { 69 | notificationSpamHandler.setDuration(storage.loadDurationOrDefault()); 70 | 71 | return; 72 | } 73 | 74 | if (getString(R.string.preferences_disabled_apps).equals(key)) { 75 | disabledApps = storage.loadDisabledApps(); 76 | 77 | return; 78 | } 79 | 80 | if (getString(R.string.preferences_display).equals(key)) { 81 | display = storage.loadDisplay(); 82 | 83 | return; 84 | } 85 | 86 | if (getString(R.string.preferences_no_app).equals(key)) { 87 | noApp = storage.loadNoApp(); 88 | } 89 | }; 90 | 91 | @Override 92 | public synchronized void onNotificationPosted(StatusBarNotification statusBarNotification) { 93 | if (!initialized) { 94 | return; 95 | } 96 | 97 | PackageManager packageManager = getPackageManager(); 98 | 99 | String packageName = statusBarNotification.getPackageName(); 100 | 101 | Log.v(TAG, "Notification posted (" + packageName + ")"); 102 | 103 | boolean test = getPackageName().equals(packageName); 104 | 105 | if (test) { 106 | Log.v(TAG, "Test notification detected"); 107 | } else { 108 | if (!noApp && packageManager.getLaunchIntentForPackage(packageName) == null) { 109 | Log.v(TAG, "Not from an actual app"); 110 | 111 | return; 112 | } 113 | 114 | if (disabledApps.contains(packageName)) { 115 | Log.v(TAG, "App is disabled"); 116 | 117 | return; 118 | } 119 | 120 | if (display && isDisplayEnabled()) { 121 | Log.v(TAG, "Display is on"); 122 | 123 | return; 124 | } 125 | } 126 | 127 | Optional optionalParsedNotification = ParsedNotification.parseNotification(statusBarNotification, this); 128 | 129 | if (!optionalParsedNotification.isPresent()) { 130 | Log.v(TAG, "Notification cannot be parsed"); 131 | 132 | return; 133 | } 134 | 135 | ParsedNotification parsedNotification = optionalParsedNotification.get(); 136 | 137 | if (notificationSpamHandler.isSpammed(parsedNotification, test)) { 138 | Log.v(TAG, "Notification is spammed"); 139 | 140 | return; 141 | } 142 | 143 | CompletableFuture.supplyAsync(() -> notificationSender.sendParsedNotification(parsedNotification)).thenAccept(keptLog -> { 144 | if (test) { 145 | storage.saveLog(keptLog.format()); 146 | } 147 | }); 148 | 149 | Log.v(TAG, "Notification given to NotificationSender"); 150 | } 151 | 152 | @Override 153 | public void onListenerConnected() { 154 | Log.v(TAG, "NotificationReceiver connected"); 155 | 156 | initialize(); 157 | } 158 | 159 | @Override 160 | public void onListenerDisconnected() { 161 | Log.v(TAG, "NotificationReceiver disconnected"); 162 | 163 | uninitialize(); 164 | } 165 | 166 | private synchronized void initialize() { 167 | if (initialized) { 168 | return; 169 | } 170 | 171 | sharedPreferences = getSharedPreferences(getString(R.string.preferences), Context.MODE_PRIVATE); 172 | storage = new Storage(this, sharedPreferences); 173 | 174 | NotificationSender.fromStorage(this, storage).ifPresent(notificationSender -> { 175 | this.notificationSender = notificationSender; 176 | 177 | notificationSpamHandler = new NotificationSpamHandler(storage.loadSimilarityOrDefault(), storage.loadDurationOrDefault()); 178 | disabledApps = storage.loadDisabledApps(); 179 | display = storage.loadDisplay(); 180 | 181 | sharedPreferences.registerOnSharedPreferenceChangeListener(listener); 182 | 183 | initialized = true; 184 | }); 185 | } 186 | 187 | private synchronized void uninitialize() { 188 | if (!initialized) { 189 | return; 190 | } 191 | 192 | sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener); 193 | } 194 | 195 | private boolean isDisplayEnabled() { 196 | for (Display display : ((DisplayManager) getSystemService(Context.DISPLAY_SERVICE)).getDisplays()) { 197 | if (display.getState() == Display.STATE_ON) { 198 | return true; 199 | } 200 | } 201 | 202 | return false; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/notification/NotificationSender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.notification; 19 | 20 | import android.content.Context; 21 | import android.util.Log; 22 | 23 | import org.zeromq.SocketType; 24 | import org.zeromq.ZContext; 25 | import org.zeromq.ZMQ; 26 | import org.zeromq.ZMsg; 27 | 28 | import java.util.List; 29 | import java.util.Optional; 30 | import java.util.concurrent.CompletableFuture; 31 | import java.util.concurrent.CountDownLatch; 32 | import java.util.concurrent.TimeUnit; 33 | import java.util.stream.Collectors; 34 | 35 | import dev.patri9ck.a2ln.R; 36 | import dev.patri9ck.a2ln.log.KeptLog; 37 | import dev.patri9ck.a2ln.server.Server; 38 | import dev.patri9ck.a2ln.util.Storage; 39 | import zmq.util.Z85; 40 | 41 | public class NotificationSender { 42 | 43 | private static final String TAG = "A2LNNS"; 44 | 45 | private static final int TIMEOUT_SECONDS = 3; 46 | 47 | private final Context context; 48 | private final byte[] publicKey; 49 | private final byte[] secretKey; 50 | private List servers; 51 | 52 | public NotificationSender(Context context, byte[] publicKey, byte[] secretKey, List servers) { 53 | this.context = context; 54 | this.publicKey = publicKey; 55 | this.secretKey = secretKey; 56 | this.servers = filterServers(servers); 57 | } 58 | 59 | public static Optional fromStorage(Context context, Storage storage) { 60 | String rawPublicKey = storage.loadRawPublicKey().orElse(null); 61 | 62 | if (rawPublicKey == null) { 63 | Log.e(TAG, "Own public key does not exist"); 64 | 65 | return Optional.empty(); 66 | } 67 | 68 | String rawSecretKey = storage.loadRawSecretKey().orElse(null); 69 | 70 | if (rawSecretKey == null) { 71 | Log.e(TAG, "Own secret key does not exist"); 72 | 73 | return Optional.empty(); 74 | } 75 | 76 | byte[] publicKey = Z85.decode(rawPublicKey); 77 | 78 | if (publicKey == null) { 79 | Log.e(TAG, "Cannot decode own public key"); 80 | 81 | return Optional.empty(); 82 | } 83 | 84 | byte[] secretKey = Z85.decode(rawSecretKey); 85 | 86 | if (secretKey == null) { 87 | Log.e(TAG, "Cannot decode own secret key"); 88 | 89 | return Optional.empty(); 90 | } 91 | 92 | return Optional.of(new NotificationSender(context, publicKey, secretKey, storage.loadServers())); 93 | } 94 | 95 | public void setServers(List servers) { 96 | this.servers = filterServers(servers); 97 | } 98 | 99 | public KeptLog sendParsedNotification(ParsedNotification parsedNotification) { 100 | KeptLog keptLog = new KeptLog(context); 101 | 102 | if (servers.isEmpty()) { 103 | keptLog.log(R.string.log_notification_no_servers); 104 | 105 | return keptLog; 106 | } 107 | 108 | keptLog.log(R.string.log_notification_trying); 109 | 110 | ZMsg zMsg = new ZMsg(); 111 | 112 | zMsg.add(parsedNotification.getAppName()); 113 | zMsg.add(parsedNotification.getTitle()); 114 | zMsg.add(parsedNotification.getText()); 115 | 116 | parsedNotification.getIcon().ifPresent(zMsg::add); 117 | 118 | CountDownLatch countDownLatch = new CountDownLatch(servers.size()); 119 | 120 | try (ZContext zContext = new ZContext()) { 121 | servers.forEach(server -> CompletableFuture.runAsync(() -> { 122 | try (ZMQ.Socket client = zContext.createSocket(SocketType.PUSH)) { 123 | client.setSendTimeOut(TIMEOUT_SECONDS * 1000); 124 | client.setImmediate(false); 125 | client.setCurvePublicKey(publicKey); 126 | client.setCurveSecretKey(secretKey); 127 | client.setCurveServerKey(server.getPublicKey()); 128 | 129 | String address = server.getAddress(); 130 | 131 | if (!client.connect("tcp://" + address)) { 132 | keptLog.log(R.string.log_failed_connection, address); 133 | } else if (!zMsg.send(client, false)) { 134 | keptLog.log(R.string.log_notification_failed_sending, address); 135 | } else { 136 | keptLog.log(R.string.log_notification_success, address); 137 | } 138 | 139 | countDownLatch.countDown(); 140 | } 141 | })); 142 | } finally { 143 | try { 144 | if (!countDownLatch.await(TIMEOUT_SECONDS + 1, TimeUnit.SECONDS)) { 145 | keptLog.log(R.string.log_notification_timed_out); 146 | } 147 | } catch (InterruptedException ignored) { 148 | // Ignored 149 | } 150 | 151 | zMsg.destroy(); 152 | } 153 | 154 | return keptLog; 155 | } 156 | 157 | private List filterServers(List servers) { 158 | return servers.stream().filter(Server::isEnabled).collect(Collectors.toList()); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/notification/ParsedNotification.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.notification; 19 | 20 | import android.app.Notification; 21 | import android.content.Context; 22 | import android.graphics.Bitmap; 23 | import android.graphics.drawable.BitmapDrawable; 24 | import android.graphics.drawable.Drawable; 25 | import android.graphics.drawable.Icon; 26 | import android.service.notification.StatusBarNotification; 27 | import android.util.Log; 28 | 29 | import java.io.ByteArrayOutputStream; 30 | import java.io.IOException; 31 | import java.util.Optional; 32 | 33 | import dev.patri9ck.a2ln.util.Util; 34 | 35 | public class ParsedNotification { 36 | 37 | private static final String TAG = "A2LN"; 38 | 39 | private final String appName; 40 | private final String title; 41 | private final String text; 42 | private final byte[] icon; 43 | 44 | public ParsedNotification(String appName, String title, String text, byte[] icon) { 45 | this.appName = appName; 46 | this.title = title; 47 | this.text = text; 48 | this.icon = icon; 49 | } 50 | 51 | public ParsedNotification(String appName, String title, String text) { 52 | this(appName, title, text, null); 53 | } 54 | 55 | public static Optional parseNotification(StatusBarNotification statusBarNotification, Context context) { 56 | Notification notification = statusBarNotification.getNotification(); 57 | 58 | String title = notification.extras.getString(Notification.EXTRA_TITLE); 59 | String text = notification.extras.getString(Notification.EXTRA_TEXT); 60 | 61 | if (title == null || text == null) { 62 | return Optional.empty(); 63 | } 64 | 65 | String appName = Util.getAppName(context.getPackageManager(), statusBarNotification.getPackageName()).orElse(""); 66 | 67 | Icon largeIcon = notification.getLargeIcon(); 68 | 69 | if (largeIcon != null) { 70 | try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { 71 | Drawable drawable = largeIcon.loadDrawable(context); 72 | 73 | if (drawable instanceof BitmapDrawable) { 74 | ((BitmapDrawable) drawable).getBitmap().compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); 75 | 76 | return Optional.of(new ParsedNotification(appName, title, text, byteArrayOutputStream.toByteArray())); 77 | } 78 | } catch (IOException exception) { 79 | Log.e(TAG, "Failed to convert picture to bytes", exception); 80 | } 81 | } 82 | 83 | return Optional.of(new ParsedNotification(appName, title, text)); 84 | } 85 | 86 | public String getAppName() { 87 | return appName; 88 | } 89 | 90 | public String getTitle() { 91 | return title; 92 | } 93 | 94 | public String getText() { 95 | return text; 96 | } 97 | 98 | public Optional getIcon() { 99 | return Optional.ofNullable(icon); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/notification/spam/NotificationSpamHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.notification.spam; 19 | 20 | import net.jodah.expiringmap.ExpirationPolicy; 21 | import net.jodah.expiringmap.ExpiringMap; 22 | 23 | import java.util.concurrent.TimeUnit; 24 | 25 | import dev.patri9ck.a2ln.notification.ParsedNotification; 26 | import dev.patri9ck.a2ln.util.Storage; 27 | import dev.patri9ck.a2ln.util.Util; 28 | 29 | public class NotificationSpamHandler { 30 | 31 | private final ExpiringMap strippedNotifications = ExpiringMap.builder() 32 | .variableExpiration() 33 | .expirationPolicy(ExpirationPolicy.CREATED) 34 | .build(); 35 | 36 | private float similarity; 37 | private int duration; 38 | 39 | public NotificationSpamHandler(float similarity, int duration) { 40 | this.similarity = similarity; 41 | this.duration = duration; 42 | } 43 | 44 | public void setSimilarity(float similarity) { 45 | this.similarity = similarity; 46 | } 47 | 48 | public void setDuration(int duration) { 49 | this.duration = duration; 50 | } 51 | 52 | public boolean isSpammed(ParsedNotification parsedNotification, boolean simple) { 53 | StrippedNotification strippedNotification = new StrippedNotification(parsedNotification); 54 | 55 | if (strippedNotifications.containsKey(strippedNotification)) { 56 | return true; 57 | } 58 | 59 | if (!simple && similarity < Storage.DEFAULT_SIMILARITY) { 60 | for (StrippedNotification spammedStrippedNotification : strippedNotifications.keySet()) { 61 | if (spammedStrippedNotification.getAppName().equals(strippedNotification.getAppName()) 62 | && spammedStrippedNotification.getTitle().equals(strippedNotification.getTitle()) 63 | && Util.getSimilarity(spammedStrippedNotification.getText(), strippedNotification.getText()) >= similarity) { 64 | strippedNotifications.put(spammedStrippedNotification, new Object(), duration, TimeUnit.SECONDS); 65 | 66 | return true; 67 | } 68 | } 69 | } 70 | 71 | strippedNotifications.put(strippedNotification, new Object(), simple ? Storage.DEFAULT_DURATION : duration, TimeUnit.SECONDS); 72 | 73 | return false; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/notification/spam/StrippedNotification.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package dev.patri9ck.a2ln.notification.spam; 20 | 21 | import java.util.Objects; 22 | 23 | import dev.patri9ck.a2ln.notification.ParsedNotification; 24 | 25 | public class StrippedNotification { 26 | 27 | private final String appName; 28 | private final String title; 29 | private final String text; 30 | 31 | public StrippedNotification(ParsedNotification parsedNotification) { 32 | this.appName = parsedNotification.getAppName(); 33 | this.title = parsedNotification.getTitle(); 34 | this.text = parsedNotification.getText(); 35 | } 36 | 37 | public String getAppName() { 38 | return appName; 39 | } 40 | 41 | public String getTitle() { 42 | return title; 43 | } 44 | 45 | public String getText() { 46 | return text; 47 | } 48 | 49 | @Override 50 | public boolean equals(Object object) { 51 | if (this == object) { 52 | return true; 53 | } 54 | 55 | if (object == null || getClass() != object.getClass()) { 56 | return false; 57 | } 58 | 59 | StrippedNotification strippedNotification = (StrippedNotification) object; 60 | 61 | return appName.equals(strippedNotification.appName) && title.equals(strippedNotification.title) && text.equals(strippedNotification.text); 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | return Objects.hash(appName, title, text); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/pairing/Pairing.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.pairing; 19 | 20 | import android.content.Context; 21 | 22 | import org.zeromq.SocketType; 23 | import org.zeromq.ZContext; 24 | import org.zeromq.ZMQ; 25 | import org.zeromq.ZMsg; 26 | 27 | import dev.patri9ck.a2ln.R; 28 | import dev.patri9ck.a2ln.log.KeptLog; 29 | import dev.patri9ck.a2ln.server.Destination; 30 | 31 | public class Pairing { 32 | 33 | private static final int TIMEOUT_SECONDS = 20; 34 | 35 | private final Context context; 36 | private final Destination destination; 37 | private final String ip; 38 | private final String rawPublicKey; 39 | 40 | public Pairing(Context context, Destination destination, String ip, String rawPublicKey) { 41 | this.context = context; 42 | this.destination = destination; 43 | this.ip = ip; 44 | this.rawPublicKey = rawPublicKey; 45 | } 46 | 47 | public PairingResult pair() { 48 | KeptLog keptLog = new KeptLog(context); 49 | 50 | String address = destination.getAddress(); 51 | 52 | keptLog.log(R.string.log_pairing_trying, address); 53 | 54 | try (ZContext zContext = new ZContext(); ZMQ.Socket client = zContext.createSocket(SocketType.REQ)) { 55 | client.setSendTimeOut(TIMEOUT_SECONDS * 1000); 56 | client.setReceiveTimeOut(TIMEOUT_SECONDS * 1000); 57 | client.setImmediate(false); 58 | 59 | if (!client.connect("tcp://" + address)) { 60 | keptLog.log(R.string.log_failed_connection, address); 61 | 62 | return new PairingResult(keptLog); 63 | } 64 | 65 | ZMsg zMsg = new ZMsg(); 66 | 67 | zMsg.add(ip); 68 | zMsg.add(rawPublicKey); 69 | 70 | if (!zMsg.send(client)) { 71 | keptLog.log(R.string.log_pairing_failed_sending, address); 72 | 73 | return new PairingResult(keptLog); 74 | } 75 | 76 | byte[] publicKey = client.recv(); 77 | 78 | if (publicKey == null) { 79 | keptLog.log(R.string.log_pairing_failed_receiving, address); 80 | 81 | return new PairingResult(keptLog); 82 | } 83 | 84 | return new PairingResult(keptLog, publicKey); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/pairing/PairingResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.pairing; 19 | 20 | import java.util.Optional; 21 | 22 | import dev.patri9ck.a2ln.log.KeptLog; 23 | 24 | public class PairingResult { 25 | 26 | private final KeptLog keptLog; 27 | private final byte[] publicKey; 28 | 29 | public PairingResult(KeptLog keptLog) { 30 | this(keptLog, null); 31 | } 32 | 33 | public PairingResult(KeptLog keptLog, byte[] publicKey) { 34 | this.keptLog = keptLog; 35 | this.publicKey = publicKey; 36 | } 37 | 38 | public KeptLog getKeptLog() { 39 | return keptLog; 40 | } 41 | 42 | public Optional getPublicKey() { 43 | return Optional.ofNullable(publicKey); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/server/Destination.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package dev.patri9ck.a2ln.server; 20 | 21 | public class Destination { 22 | 23 | private String ip; 24 | private int port; 25 | 26 | public Destination() { 27 | // Gson 28 | } 29 | 30 | public Destination(String ip, int port) { 31 | this.ip = ip; 32 | this.port = port; 33 | } 34 | 35 | public String getIp() { 36 | return ip; 37 | } 38 | 39 | public void setIp(String ip) { 40 | this.ip = ip; 41 | } 42 | 43 | public int getPort() { 44 | return port; 45 | } 46 | 47 | public void setPort(int port) { 48 | this.port = port; 49 | } 50 | 51 | public String getAddress() { 52 | return ip + ":" + port; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/server/DragAndDropCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.server; 19 | 20 | import androidx.annotation.NonNull; 21 | import androidx.recyclerview.widget.ItemTouchHelper; 22 | import androidx.recyclerview.widget.RecyclerView; 23 | 24 | import java.util.Collections; 25 | import java.util.List; 26 | 27 | import dev.patri9ck.a2ln.util.Storage; 28 | 29 | public class DragAndDropCallback extends ItemTouchHelper.SimpleCallback { 30 | 31 | private final List servers; 32 | private final Storage storage; 33 | private final ServersAdapter serversAdapter; 34 | 35 | public DragAndDropCallback(List servers, Storage storage, ServersAdapter serversAdapter) { 36 | super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0); 37 | 38 | this.servers = servers; 39 | this.storage = storage; 40 | this.serversAdapter = serversAdapter; 41 | } 42 | 43 | @Override 44 | public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { 45 | int from = viewHolder.getAdapterPosition(); 46 | int to = target.getAdapterPosition(); 47 | 48 | Collections.swap(servers, from, to); 49 | 50 | serversAdapter.notifyItemMoved(from, to); 51 | 52 | storage.saveServers(servers); 53 | 54 | return true; 55 | } 56 | 57 | @Override 58 | public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { 59 | // Ignored 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/server/Server.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.server; 19 | 20 | import java.util.Optional; 21 | 22 | public class Server extends Destination { 23 | 24 | private byte[] publicKey; 25 | private String alias; 26 | private boolean enabled; 27 | 28 | public Server() { 29 | // Gson 30 | } 31 | 32 | public Server(String ip, int port, byte[] publicKey, String alias, boolean enabled) { 33 | super(ip, port); 34 | 35 | this.publicKey = publicKey; 36 | this.alias = alias; 37 | this.enabled = enabled; 38 | } 39 | 40 | public Server(String ip, int port, byte[] publicKey) { 41 | this(ip, port, publicKey, null, true); 42 | } 43 | 44 | public byte[] getPublicKey() { 45 | return publicKey; 46 | } 47 | 48 | public Optional getAlias() { 49 | return Optional.ofNullable(alias); 50 | } 51 | 52 | public void setAlias(String alias) { 53 | this.alias = alias; 54 | } 55 | 56 | public boolean isEnabled() { 57 | return enabled; 58 | } 59 | 60 | public void setEnabled(boolean enabled) { 61 | this.enabled = enabled; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/server/ServersAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.server; 19 | 20 | import android.annotation.SuppressLint; 21 | import android.view.LayoutInflater; 22 | import android.view.ViewGroup; 23 | import android.widget.CheckBox; 24 | import android.widget.TextView; 25 | 26 | import androidx.annotation.NonNull; 27 | import androidx.recyclerview.widget.RecyclerView; 28 | 29 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 30 | 31 | import java.util.List; 32 | 33 | import dev.patri9ck.a2ln.R; 34 | import dev.patri9ck.a2ln.databinding.DialogEditServerBinding; 35 | import dev.patri9ck.a2ln.databinding.ItemServerBinding; 36 | import dev.patri9ck.a2ln.util.Storage; 37 | 38 | public class ServersAdapter extends RecyclerView.Adapter { 39 | 40 | private final ServersFragment serversFragment; 41 | private final Storage storage; 42 | private final List servers; 43 | 44 | public ServersAdapter(ServersFragment serversFragment, Storage storage, List servers) { 45 | this.serversFragment = serversFragment; 46 | this.storage = storage; 47 | this.servers = servers; 48 | } 49 | 50 | @NonNull 51 | @Override 52 | public ServerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 53 | return new ServerViewHolder(ItemServerBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); 54 | } 55 | 56 | @SuppressLint("SetTextI18n") 57 | @Override 58 | public void onBindViewHolder(ServerViewHolder holder, int position) { 59 | Server server = servers.get(position); 60 | 61 | holder.addressTextView.setText(server.getAlias().orElse(server.getAddress())); 62 | 63 | holder.addressTextView.setOnClickListener(view -> { 64 | DialogEditServerBinding dialogEditServerBinding = DialogEditServerBinding.inflate(serversFragment.getLayoutInflater()); 65 | 66 | dialogEditServerBinding.editServerIpEditText.setText(server.getIp()); 67 | dialogEditServerBinding.editServerPortEditText.setText(Integer.toString(server.getPort())); 68 | 69 | server.getAlias().ifPresent(dialogEditServerBinding.editServerAliasEditText::setText); 70 | 71 | new MaterialAlertDialogBuilder(serversFragment.requireContext(), R.style.Dialog) 72 | .setTitle(R.string.edit_server_dialog_title) 73 | .setView(dialogEditServerBinding.getRoot()) 74 | .setPositiveButton(R.string.apply, (editPortDialog, which) -> { 75 | String ip = dialogEditServerBinding.editServerIpEditText.getText().toString(); 76 | 77 | serversFragment.validate(ip, dialogEditServerBinding.editServerPortEditText.getText().toString(), !ip.equals(server.getIp())).ifPresent(destination -> { 78 | String alias = dialogEditServerBinding.editServerAliasEditText.getText().toString(); 79 | 80 | server.setAlias(alias.trim().isEmpty() ? null : alias); 81 | server.setIp(destination.getIp()); 82 | server.setPort(destination.getPort()); 83 | 84 | notifyItemChanged(position); 85 | 86 | storage.saveServers(servers); 87 | }); 88 | }) 89 | .setNegativeButton(R.string.cancel, null) 90 | .show(); 91 | }); 92 | 93 | holder.serverCheckBox.setOnCheckedChangeListener((serverCheckBoxView, isChecked) -> { 94 | server.setEnabled(isChecked); 95 | 96 | storage.saveServers(servers); 97 | }); 98 | 99 | holder.serverCheckBox.setChecked(server.isEnabled()); 100 | } 101 | 102 | @Override 103 | public int getItemCount() { 104 | return servers.size(); 105 | } 106 | 107 | protected static class ServerViewHolder extends RecyclerView.ViewHolder { 108 | 109 | private final TextView addressTextView; 110 | private final CheckBox serverCheckBox; 111 | 112 | public ServerViewHolder(ItemServerBinding itemServerBinding) { 113 | super(itemServerBinding.getRoot()); 114 | 115 | addressTextView = itemServerBinding.addressTextView; 116 | serverCheckBox = itemServerBinding.serverCheckBox; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/server/SwipeToDeleteCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.server; 19 | 20 | import android.view.View; 21 | 22 | import androidx.annotation.NonNull; 23 | import androidx.recyclerview.widget.ItemTouchHelper; 24 | import androidx.recyclerview.widget.RecyclerView; 25 | 26 | import com.google.android.material.snackbar.Snackbar; 27 | 28 | import java.util.List; 29 | 30 | import dev.patri9ck.a2ln.R; 31 | import dev.patri9ck.a2ln.util.Storage; 32 | 33 | public class SwipeToDeleteCallback extends ItemTouchHelper.SimpleCallback { 34 | 35 | private final View rootView; 36 | private final Storage storage; 37 | private final List servers; 38 | private final ServersAdapter serversAdapter; 39 | 40 | public SwipeToDeleteCallback(View rootView, Storage storage, List servers, ServersAdapter serversAdapter) { 41 | super(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT); 42 | 43 | this.rootView = rootView; 44 | this.storage = storage; 45 | this.servers = servers; 46 | this.serversAdapter = serversAdapter; 47 | } 48 | 49 | @Override 50 | public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { 51 | return false; 52 | } 53 | 54 | @Override 55 | public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { 56 | int position = viewHolder.getAdapterPosition(); 57 | 58 | Server server = servers.remove(position); 59 | 60 | serversAdapter.notifyItemRemoved(position); 61 | 62 | storage.saveServers(servers); 63 | 64 | Snackbar.make(rootView, R.string.removed_server, Snackbar.LENGTH_LONG) 65 | .setAction(R.string.removed_server_undo, view -> { 66 | servers.add(position, server); 67 | serversAdapter.notifyItemInserted(position); 68 | 69 | storage.saveServers(servers); 70 | }).show(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/util/Storage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package dev.patri9ck.a2ln.util; 20 | 21 | import android.content.Context; 22 | import android.content.SharedPreferences; 23 | 24 | import java.util.List; 25 | import java.util.Optional; 26 | 27 | import dev.patri9ck.a2ln.R; 28 | import dev.patri9ck.a2ln.server.Server; 29 | 30 | public class Storage { 31 | 32 | public static final float DEFAULT_SIMILARITY = 1F; 33 | public static final int DEFAULT_DURATION = 1; 34 | 35 | public static final int DEFAULT_PORT = 23045; 36 | 37 | private final Context context; 38 | private final SharedPreferences sharedPreferences; 39 | 40 | public Storage(Context context, SharedPreferences sharedPreferences) { 41 | this.context = context; 42 | this.sharedPreferences = sharedPreferences; 43 | } 44 | 45 | public List loadServers() { 46 | return Util.fromJson(sharedPreferences.getString(context.getString(R.string.preferences_servers), null), Server.class); 47 | } 48 | 49 | public void saveServers(List servers) { 50 | sharedPreferences.edit().putString(context.getString(R.string.preferences_servers), Util.toJson(servers)).apply(); 51 | } 52 | 53 | public Optional loadRawPublicKey() { 54 | return Optional.ofNullable(sharedPreferences.getString(context.getString(R.string.preferences_public_key), null)); 55 | } 56 | 57 | public Optional loadRawSecretKey() { 58 | return Optional.ofNullable(sharedPreferences.getString(context.getString(R.string.preferences_secret_key), null)); 59 | } 60 | 61 | public List loadDisabledApps() { 62 | return Util.fromJson(sharedPreferences.getString(context.getString(R.string.preferences_disabled_apps), null), String.class); 63 | } 64 | 65 | public void saveDisabledApps(List disabledApps) { 66 | sharedPreferences.edit().putString(context.getString(R.string.preferences_disabled_apps), Util.toJson(disabledApps)).apply(); 67 | } 68 | 69 | public Optional loadSimilarity() { 70 | float similarity = sharedPreferences.getFloat(context.getString(R.string.preferences_similarity), Float.MIN_VALUE); 71 | 72 | return Optional.ofNullable(similarity == Float.MIN_VALUE ? null : similarity); 73 | } 74 | 75 | public float loadSimilarityOrDefault() { 76 | return loadSimilarity().orElse(DEFAULT_SIMILARITY); 77 | } 78 | 79 | public void saveSimilarity(float similarity) { 80 | sharedPreferences.edit().putFloat(context.getString(R.string.preferences_similarity), similarity).apply(); 81 | } 82 | 83 | public void removeSimilarity() { 84 | sharedPreferences.edit().remove(context.getString(R.string.preferences_similarity)).apply(); 85 | } 86 | 87 | public Optional loadDuration() { 88 | int duration = sharedPreferences.getInt(context.getString(R.string.preferences_duration), Integer.MIN_VALUE); 89 | 90 | return Optional.ofNullable(duration == Integer.MIN_VALUE ? null : duration); 91 | } 92 | 93 | public int loadDurationOrDefault() { 94 | return loadDuration().orElse(DEFAULT_DURATION); 95 | } 96 | 97 | public void saveDuration(int duration) { 98 | sharedPreferences.edit().putInt(context.getString(R.string.preferences_duration), duration).apply(); 99 | } 100 | 101 | public void removeDuration() { 102 | sharedPreferences.edit().remove(context.getString(R.string.preferences_duration)).apply(); 103 | } 104 | 105 | public boolean loadDisplay() { 106 | return sharedPreferences.getBoolean(context.getString(R.string.preferences_display), false); 107 | } 108 | 109 | public void saveDisplay(boolean display) { 110 | sharedPreferences.edit().putBoolean(context.getString(R.string.preferences_display), display).apply(); 111 | } 112 | 113 | public boolean loadNoApp() { 114 | return sharedPreferences.getBoolean(context.getString(R.string.preferences_no_app), false); 115 | } 116 | 117 | public void saveNoApp(boolean noApp) { 118 | sharedPreferences.edit().putBoolean(context.getString(R.string.preferences_no_app), noApp).apply(); 119 | } 120 | 121 | public Optional loadLog() { 122 | return Optional.ofNullable(sharedPreferences.getString(context.getString(R.string.preferences_log), null)); 123 | } 124 | 125 | public void saveLog(String log) { 126 | sharedPreferences.edit().putString(context.getString(R.string.preferences_log), log).apply(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/dev/patri9ck/a2ln/util/Util.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Android 2 Linux Notifications - A way to display Android phone notifications on Linux 3 | * Copyright (C) 2023 patri9ck and contributors 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package dev.patri9ck.a2ln.util; 19 | 20 | import android.content.pm.PackageManager; 21 | 22 | import com.google.gson.Gson; 23 | import com.google.gson.reflect.TypeToken; 24 | 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | import java.util.Optional; 28 | 29 | import me.xdrop.fuzzywuzzy.FuzzySearch; 30 | 31 | public class Util { 32 | 33 | private static final int MINIMUM_PORT = 1; 34 | private static final int MAXIMUM_PORT = 65535; 35 | 36 | private static final Gson GSON = new Gson(); 37 | 38 | private Util() { 39 | } 40 | 41 | public static String toJson(List raw) { 42 | return GSON.toJson(raw); 43 | } 44 | 45 | public static List fromJson(String json, Class type) { 46 | if (json == null) { 47 | return new ArrayList<>(); 48 | } 49 | 50 | return GSON.fromJson(json, TypeToken.getParameterized(ArrayList.class, type).getType()); 51 | } 52 | 53 | public static Optional parseInteger(String rawInteger) { 54 | try { 55 | return Optional.of(Integer.parseInt(rawInteger)); 56 | } catch (NumberFormatException ignored) { 57 | return Optional.empty(); 58 | } 59 | } 60 | 61 | public static Optional parsePort(String rawPort) { 62 | return parseInteger(rawPort).filter(port -> port >= MINIMUM_PORT && port <= MAXIMUM_PORT); 63 | } 64 | 65 | public static Optional getAppName(PackageManager packageManager, String packageName) { 66 | try { 67 | return Optional.of((String) packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName, 0))); 68 | } catch (PackageManager.NameNotFoundException ignored) { 69 | return Optional.empty(); 70 | } 71 | } 72 | 73 | public static float getSimilarity(String first, String second) { 74 | return (float) FuzzySearch.ratio(first, second) / 100F; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_apps.xml: -------------------------------------------------------------------------------- 1 | 18 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_help.xml: -------------------------------------------------------------------------------- 1 | 18 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notification.xml: -------------------------------------------------------------------------------- 1 | 18 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pair.xml: -------------------------------------------------------------------------------- 1 | 18 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_permission.xml: -------------------------------------------------------------------------------- 1 | 18 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_qr_code.xml: -------------------------------------------------------------------------------- 1 | 18 | 23 | 26 | 29 | 32 | 35 | 38 | 41 | 44 | 47 | 50 | 53 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_servers.xml: -------------------------------------------------------------------------------- 1 | 18 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 18 | 24 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 18 | 24 | 25 | 29 | 30 | 35 | 36 | 37 | 38 | 46 | 47 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_edit_server.xml: -------------------------------------------------------------------------------- 1 | 18 | 23 | 24 | 28 | 29 | 38 | 39 | 40 | 41 | 45 | 46 | 54 | 55 | 56 | 57 | 61 | 62 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_pair.xml: -------------------------------------------------------------------------------- 1 | 18 | 22 | 23 | 28 | 29 | 37 | 38 | 43 | 44 | 51 | 52 | 53 | 54 | 59 | 60 | 67 | 68 | 69 | 70 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_paired.xml: -------------------------------------------------------------------------------- 1 | 18 | 24 | 25 | 30 | 31 | 38 | 39 | 44 | 45 | 51 | 52 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_pairing.xml: -------------------------------------------------------------------------------- 1 | 18 | 24 | 25 | 32 | 33 | 38 | 39 | 46 | 47 | 52 | 53 | 59 | 60 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_apps.xml: -------------------------------------------------------------------------------- 1 | 18 | 23 | 24 | 31 | 32 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_servers.xml: -------------------------------------------------------------------------------- 1 | 18 | 23 | 24 | 28 | 29 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_settings.xml: -------------------------------------------------------------------------------- 1 | 18 | 26 | 27 | 31 | 32 | 36 | 37 | 45 | 46 |