├── .github └── workflows │ └── android.yml ├── .gitmodules ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── com │ │ └── jvdegithub │ │ └── aiscatcher │ │ ├── AisCatcherJava.java │ │ ├── AisService.java │ │ ├── DeviceManager.java │ │ ├── LocationHelper.java │ │ ├── MainActivity.java │ │ ├── Settings.java │ │ ├── tools │ │ ├── InputFilterIP.java │ │ ├── InputFilterMinMax.java │ │ └── LogBook.java │ │ └── ui │ │ └── main │ │ ├── StatisticsFragment.java │ │ └── WebViewMapFragment.java │ ├── jni │ ├── CMakeLists.txt │ └── JNI │ │ └── AIScatcherNDK.cpp │ └── res │ ├── color │ └── bottom_navigation_colors.xml │ ├── drawable-anydpi-v24 │ └── ic_notif_launcher.xml │ ├── drawable-hdpi │ └── ic_notif_launcher.png │ ├── drawable-mdpi │ └── ic_notif_launcher.png │ ├── drawable-xhdpi │ └── ic_notif_launcher.png │ ├── drawable-xxhdpi │ └── ic_notif_launcher.png │ ├── drawable │ ├── ic_baseline_clear_40.xml │ ├── ic_baseline_content_copy_24.xml │ ├── ic_baseline_input_40.xml │ ├── ic_baseline_play_circle_filled_40.xml │ ├── ic_baseline_settings_24.xml │ ├── ic_baseline_stop_circle_40.xml │ ├── ic_baseline_web_asset_24.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ └── logo.xml │ ├── layout │ ├── activity_main.xml │ ├── fragment_log.xml │ ├── fragment_map.xml │ ├── fragment_nmealog.xml │ └── fragment_statistics.xml │ ├── menu │ ├── bottom_menu.xml │ └── toolbar_menu.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-land │ └── dimens.xml │ ├── values-night │ └── themes.xml │ ├── values-w1240dp │ └── dimens.xml │ ├── values-w600dp │ └── dimens.xml │ ├── values-w820dp │ └── dimens.xml │ ├── values │ ├── arrays.xml │ ├── colors.xml │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ ├── data_extraction_rules.xml │ ├── network_security_config.xml │ ├── preferences.xml │ ├── tool_bar.xml │ └── usb_device_filter.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | apk: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository and submodules 16 | uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: set up JDK 17 21 | uses: actions/setup-java@v3 22 | with: 23 | java-version: '17' 24 | distribution: 'temurin' 25 | cache: gradle 26 | 27 | - name: Setup Android SDK 28 | uses: android-actions/setup-android@v3 29 | 30 | - name: Install Ninja and check Android SDK 31 | run: | 32 | sudo apt-get update 33 | sudo apt-get install -y ninja-build 34 | 35 | - name: Set permissions for gradlew 36 | run: chmod +x gradlew 37 | 38 | - name: Check Android SDK Details 39 | run: | 40 | echo "SDK Location:" 41 | echo $ANDROID_SDK_ROOT 42 | echo "Build Tools:" 43 | ls -la $ANDROID_SDK_ROOT/build-tools/ 44 | echo "Platform Tools:" 45 | ls -la $ANDROID_SDK_ROOT/platform-tools/ 46 | echo "NDK versions:" 47 | ls -la $ANDROID_SDK_ROOT/ndk/ 48 | 49 | - name: Build 50 | run: ./gradlew build 51 | 52 | - uses: ilharp/sign-android-release@v1 53 | name: Sign app APK 54 | # ID used to access action output 55 | id: sign_app 56 | with: 57 | releaseDir: app/build/outputs/apk/release 58 | signingKey: ${{ secrets.SIGNING_KEY }} 59 | keyAlias: ${{ secrets.ALIAS }} 60 | keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} 61 | keyPassword: ${{ secrets.KEY_PASSWORD }} 62 | buildToolsVersion: 34.0.0 63 | 64 | - name: Upload Debug APK 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: debug-apk 68 | path: app/build/outputs/apk/debug/app-debug.apk 69 | 70 | - name: Upload Release APK 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: release-apk 74 | path: ${{steps.sign_app.outputs.signedFile}} 75 | 76 | - name: Upload to Edge relaese 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 79 | run: | 80 | gh release upload Edge app/build/outputs/apk/debug/app-debug.apk --clobber 81 | gh release upload Edge ${{steps.sign_app.outputs.signedFile}} --clobber 82 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "app/src/main/jni/libusb"] 2 | path = app/src/main/jni/libusb 3 | url = https://github.com/libusb/libusb.git 4 | [submodule "app/src/main/jni/AIS-catcher"] 5 | path = app/src/main/jni/AIS-catcher 6 | url = https://github.com/jvde-github/AIS-catcher.git 7 | [submodule "app/src/main/jni/rtl-sdr"] 8 | path = app/src/main/jni/rtl-sdr 9 | url = https://github.com/jvde-github/rtl-sdr.git 10 | [submodule "app/src/main/jni/airspyone_host"] 11 | path = app/src/main/jni/airspyone_host 12 | url = https://github.com/jvde-github/airspyone_host.git 13 | [submodule "app/src/main/jni/airspyhf"] 14 | path = app/src/main/jni/airspyhf 15 | url = https://github.com/jvde-github/airspyhf.git 16 | [submodule "app/src/main/assets/webassets"] 17 | path = app/src/main/assets/webassets 18 | url = https://github.com/jvde-github/webassets.git 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AIS-catcher for Android - A multi-platform AIS receiver 2 | This Android App helps to change your Android device into a dual channel AIS receiver that can be used to pick up AIS signals from nearby vessels, even if offline! 3 | The App directly accesses a Software Defined Radio USB device, like a RTL-SDR dongle or an AirSpy decvice. Received vessels are visualized on the built-in map or messages are sent via UDP to plotting Apps like [Boat Beacon](https://pocketmariner.com/mobile-apps/boatbeacon/) or [OpenCPN](https://play.google.com/store/apps/details?id=org.opencpn.opencpn_free). A lightweight AIS receiver system when travelling. AIS-catcher for Android has been tested on an Odroid running Android. 4 | 5 | An impression of AIS-catcher on the beach on a Galaxy Note 20 in July 2023 (thanks and credit: Roger G7RUH) 6 |

7 | 8 | 9 |

10 | 11 | 12 | Here you can find a [link](https://github.com/jvde-github/AIS-catcher-for-Android/releases/download/Edge/app-release-signed.apk) to the APK file for latest Edge version or visit the [Google Play Store](https://play.google.com/store/apps/details?id=com.jvdegithub.aiscatcher&gl=NL). The engine and visualizations are based on [AIS-catcher](https://github.com/jvde-github/AIS-catcher). 13 | 14 | > ***NOTE: The Google Play Store introduced new requirements for developers to publish their personal details like address which we dont want to adhere to. Hence the app will be no longer available in the Play Store from mid August. The APK can still be downloaded and installed from here.*** 15 | 16 | AIS-catcher had a recent overhaul. The instructions below still are relevant but the visualization of the results is now based on the same code as the AIS-catcher web interface. The instructions will be updated in due course. 17 |

18 |   19 |   20 |  
21 |   22 |   23 |   24 |

25 | 26 | The requirements to receive AIS signals are: a RTL-SDR dongle (or alternatively an AirSpy Mini/R2/HF+), a simple antenna, 27 | an Android device with USB connector and an OTG cable to connect the dongle with your Android device. 28 | AIS-catcher only receives and processes signals and then forwards the messages over UDP or visualizes them on the build-in map (internet connection required). 29 | And one more thing, you need to be in a region where there are ships broadcasting AIS signals, e.g. near the water. 30 | 31 | ### What's New? 32 | 33 | - GUI has now been aligned with latest AIS-catcher versions 34 | - Auto Start option 35 | - Option to provide a web viewer at a defined port 36 | 37 |

38 |

39 | 40 | ### Purpose and Disclaimer 41 | 42 | ```AIS-catcher for Android``` is created for research and educational purposes under the GPL license. 43 | This program comes with **ABSOLUTELY NO WARRANTY**; This is free software, and you are welcome to redistribute it 44 | under certain conditions. For details see the [project page](https://github.com/jvde-github/AIS-catcher-for-Android). 45 | 46 | It is a hobby project and not tested and designed for reliability and correctness. 47 | You can play with the software but it is the user's responsibility to use it prudently. So, **DO NOT** rely upon this software in any way including for navigation 48 | and/or safety of life or property purposes. 49 | There are variations in the legislation concerning radio reception in the different administrations around the world. 50 | It is your responsibility to determine whether or not your local administration permits the reception and handling of AIS messages from ships and you can have this App on your phone. 51 | It is specifically forbidden to use this software for any illegal purpose whatsoever. 52 | The software is intended for use **only in those regions** where such use **is permitted**. 53 | 54 | ## Installation and Download 55 | 56 | You can download AIS-catcher-for-Android in the [Release section](https://github.com/jvde-github/AIS-catcher-for-Android/releases) in the form of an APK-file. There are various resources on how to install an APK file available on the [web](https://www.androidauthority.com/how-to-install-apks-31494/). 57 | 58 | Some Android manufacturers prefer battery life over proper functionality of Apps which might be particular relevant for a SDR AIS receiver. You can find tips for various devices at [https://dontkillmyapp.com/](https://dontkillmyapp.com/). 59 | 60 | For a video of a field test of an early version [see YouTube](https://www.youtube.com/shorts/1ArB7GL_yV8). Below we have included a Getting Started tutorial when running with a RTL-SDR dongle. The steps for the AirSpy and TCP connections are very similar. Please notice that your phone or tablet has to power the USB device and run the decoding algorithm and this will be a drain on your battery. When sending UDP NMEA lines over the network or decoding from TCP (SpyServer or RTL-TCP) this will require serious network traffic. Advice is to do this when connected via WiFi. 61 | Finally, the computationally intensive nature of AIS decoding requires the phone to give the Application sufficient run time. On some phones Android might restrict this and some tuning of the phone 62 | settings might be required. 63 | 64 | 65 | ## Tutorial: Getting Started 66 | 67 | ### 1. Getting around: the main screen 68 | The main screen of AIS-catcher is as follows: 69 |

70 | 71 |

72 | The tabs section at the top provides access to the main statistics and information when AIS-catcher is running whereas the bottom navigation bar has the buttons to start/stop the receiver, clear the logs/statistics and select the source device you want to use for reception. 73 | 74 | ### 2. Configuring the connection with OpenCPN and/or BoatBeacon 75 | AIS-catcher is a simple receiver that decodes messages and can send the messages from ships onward to specialized plotting apps via UDP. 76 | In this step we are going to set up the outward connections to BoatBeacon and OpenCPN. For this we will use port ``10110`` for BoatBeacon and ``10111`` for OpenCPN. Press the 3 vertical dots on the top right and select the **Setting** option. Scroll to the bottom and activate the two UDP output connections via the switch and set the parameters as follows: 77 |

78 | 79 |

80 | 81 | ### 3. Setting up the Connection on OpenCPN 82 | Next we start OpenCPN and click on **Options** (top left icon) and choose the **Connections** tab. We need to add a Network connection using UDP at address ``0.0.0.0`` dataport ``10111``. 83 | You could initially select ``Show NMEA Debug Window`` as extra option which will give you a window in OpenCPN that shows all incoming NMEA messages it receives from AIS-catcher. This could be helpful 84 | debugging a connection issue between the receiver and OpenCPN. The **Connections** tab will look something like: 85 |

86 | 87 |

88 | 89 | ### 4. Granting AIS-catcher access to the USB dongle 90 | Next we connect AIS-catcher to the RTL-SDR dongle. By default the user needs to give applications the rights to use a USB device. 91 | For this connect the dongle with your Android device using the OTG cable (if needed) and, if all is well, you should be asked if AIS-catcher can get access. 92 | With Dutch language settings (sorry) this should look like: 93 | 94 |

95 | 96 |

97 | 98 | Accept the request and consider giving AIS-catcher permanent access to the device so this step can be skipped in the future. 99 | 100 | ### 5. Configuring the RTL-SDR dongle 101 | Next go back to the **Settings** menu via the 3 vertical dots on the main screen and set up the RTL-SDR settings: 102 | 103 |

104 | 105 |

106 | 107 | These settings should be ok but don't forget to set the frequency correction in PPM if needed for your device. You can set the dongle settings at any point in time but they will only 108 | become active when a new run is started. 109 | 110 | ### 6. Selecting the input source 111 | In the Main screen select the **Source** by clicking the right-most item in the bottom navigation bar. Select the RTL-SDR device: 112 |

113 | 114 |

115 | 116 | The bottom navigation bar should show which device is currently active and will be used for AIS reception. 117 | 118 | ### 7. Running AIS-catcher 119 | In the main screen now click **Start** on the left in the bottom navigation bar. This starts the run and a notification is given that a foreground service is started. 120 | The navigation tabs allow you to see different statistics during the run (like message count (STAT), messages from the receiver (LOG) and received NMEA lines (NMEA) ). 121 |

122 | 123 | 124 | 125 | 126 |

127 | 128 | AIS-catcher will run as a foreground service so the app will continue to receive messages when closed. That's all there is to it. Have fun! 129 | 130 | ## Credits 131 | 132 | AIS-catcher for Android uses the following libraries: 133 | 134 | **libusb-1.0.26+**: https://github.com/libusb/libusb 135 | 136 | libusb is a library for USB device access from Linux, macOS, Windows, OpenBSD/NetBSD, Haiku and Solaris userspace. It is written in C (Haiku backend in C++) and licensed under the GNU Lesser General Public License version 2.1 or, at your option, any later version (see COPYING). 137 | 138 | **rtl-sdr**: https://github.com/osmocom/rtl-sdr 139 | 140 | Turns your Realtek RTL2832 based DVB dongle into a SDR receiver. Licensed under the GPL-2.0 license. 141 | Modified for Android to open devices with file descriptors: https://github.com/jvde-github/rtl-sdr. 142 | 143 | **airspyhf**: https://github.com/airspy/airspyhf 144 | 145 | This repository contains host software (Linux/Windows) for Airspy HF+, a high performance software defined radio for the HF and VHF bands. Licensed under the BSD-3-Clause license. Modified for Android to open devices with file descriptors: https://github.com/jvde-github/airspyhf. 146 | 147 | **airspyone_host**: https://github.com/airspy/airspyone_host 148 | 149 | AirSpy usemode driver and associated tools. Modified for file descriptors here: https://github.com/jvde-github/airspyone_host. 150 | 151 | **AIS-catcher**: https://github.com/jvde-github/AIS-catcher 152 | 153 | AIS receiver for RTL SDR dongles, Airspy R2, Airspy Mini, Airspy HF+, HackRF and SDRplay. Licensed under the GPGL license. 154 | 155 | ## Privacy Policy 156 | At the moment we don't collect any user data. This policy will vary per version so please check this policy for each release. 157 | 158 | ## To do 159 | 160 | - More testing.... 161 | - Application crashes when USB device is unplugged whilst in the source selection menu 162 | - Application should automatically switch to SDR source if not playing and device connected 163 | - Application crashes when AirSpy HF+ is disconnected (seems to be a more general issue) Solved. 164 | - Shorter timeouts when connecting to RTL-TCP 165 | - WiFi-only check in case: RTL-TCP streaming or UDP NMEA broadcast to other machines 166 | - Wakelocks and WiFi performance settings, etc... 167 | - Add sync locks for updates 168 | - Count buffer under- and over-runs 169 | - Simple map - radar view 170 | - Simple graphs with statistics 171 | - Start button not properly reset when receiver stops due to timeout? 172 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | 7 | compileSdk 34 8 | 9 | defaultConfig { 10 | applicationId "com.jvdegithub.aiscatcher" 11 | minSdk 23 12 | targetSdk 34 13 | versionCode 106 14 | versionName '1.06' 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | externalNativeBuild { 18 | cmake { 19 | cppFlags '' 20 | } 21 | } 22 | signingConfig signingConfigs.debug 23 | } 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | buildFeatures { 36 | viewBinding true 37 | } 38 | dataBinding{ 39 | enabled=true 40 | } 41 | sourceSets { 42 | main { 43 | java { 44 | srcDirs 'src\\main\\java', 'src\\main\\java\\2' 45 | } 46 | } 47 | } 48 | externalNativeBuild { 49 | cmake { 50 | path file('src/main/jni/CMakeLists.txt') 51 | version '3.18.1+' 52 | } 53 | } 54 | ndkVersion '24.0.8215888' 55 | namespace 'com.jvdegithub.aiscatcher' 56 | } 57 | 58 | dependencies { 59 | 60 | implementation 'androidx.appcompat:appcompat:1.7.0' 61 | implementation 'com.google.android.material:material:1.12.0' 62 | implementation 'androidx.constraintlayout:constraintlayout:2.2.0' 63 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7' 64 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7' 65 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 66 | implementation 'androidx.preference:preference:1.2.1' 67 | implementation 'androidx.webkit:webkit:1.12.1' 68 | testImplementation 'junit:junit:4.13.2' 69 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 70 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 71 | } 72 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 49 | 50 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/jvdegithub/aiscatcher/AisCatcherJava.java: -------------------------------------------------------------------------------- 1 | /* 2 | * AIS-catcher for Android 3 | * Copyright (C) 2022-2023 jvde.github@gmail.com. 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 com.jvdegithub.aiscatcher; 20 | 21 | import java.text.DecimalFormat; 22 | 23 | public class AisCatcherJava { 24 | 25 | public interface AisCallback { 26 | 27 | void onNMEA(final String line); 28 | 29 | void onConsole(final String line); 30 | 31 | void onError(final String line); 32 | 33 | void onUpdate(); 34 | 35 | void onMessage(final String line); 36 | } 37 | 38 | private static AisCallback callback = null; 39 | 40 | static native int InitNative(int port); 41 | 42 | static native String getLibraryVersion(); 43 | 44 | static native int createReceiver(int source, int FD, int CGF_wide, int model_type, int FPDS); 45 | 46 | static native int Run(); 47 | 48 | static native int Close(); 49 | 50 | static native int forceStop(); 51 | 52 | static native boolean isStreaming(); 53 | 54 | static native int applySetting(String dev, String Setting, String Param); 55 | 56 | static native int createUDP(String h, String p, boolean JSON); 57 | 58 | static native int createTCPlistener(String p); 59 | static native int createWebViewer(String p); 60 | 61 | static native int createSharing(boolean b, String key); 62 | 63 | 64 | static native int getSampleRate(); 65 | 66 | static native String getRateDescription(); 67 | 68 | static native void setLatLon(float lat,float lon); 69 | 70 | static native void setDeviceDescription(String p, String v, String s); 71 | 72 | public static class Statistics { 73 | 74 | private static int DataB = 0; 75 | private static int DataGB = 0; 76 | private static int Total = 0; 77 | private static int ChA = 0; 78 | private static int ChB = 0; 79 | private static int Msg123 = 0; 80 | private static int Msg5 = 0; 81 | private static int Msg1819 = 0; 82 | private static int Msg24 = 0; 83 | private static int MsgOther = 0; 84 | 85 | public static int getDataB() { 86 | return DataB; 87 | } 88 | 89 | public static int getDataGB() { 90 | return DataGB; 91 | } 92 | 93 | public static String getDataString() { 94 | DecimalFormat df = new DecimalFormat("0.0"); 95 | 96 | if (DataGB != 0) { 97 | float data = (float) getDataGB() + (float) getDataB() / 1000000000.0f; 98 | return df.format(data) + " GB"; 99 | } else { 100 | float data = (float) getDataB() / 1000000.0f; 101 | return df.format(data) + " MB"; 102 | } 103 | } 104 | 105 | public static int getTotal() { 106 | return Total; 107 | } 108 | 109 | public static int getChA() { 110 | return ChA; 111 | } 112 | 113 | public static int getChB() { 114 | return ChB; 115 | } 116 | 117 | public static int getMsg123() { 118 | return Msg123; 119 | } 120 | 121 | public static int getMsg5() { 122 | return Msg5; 123 | } 124 | 125 | public static int getMsg1819() { 126 | return Msg1819; 127 | } 128 | 129 | public static int getMsg24() { 130 | return Msg24; 131 | } 132 | 133 | public static int getMsgOther() { 134 | return MsgOther; 135 | } 136 | 137 | private static native void Init(); 138 | 139 | static native void Reset(); 140 | } 141 | 142 | public static void Init(int port) { 143 | InitNative(port); 144 | Statistics.Init(); 145 | } 146 | 147 | public static void registerCallback(AisCallback m) { 148 | callback = m; 149 | } 150 | 151 | public static void unregisterCallback() { 152 | callback = null; 153 | } 154 | 155 | public static void Reset() { 156 | 157 | Statistics.Reset(); 158 | } 159 | 160 | private static void onNMEA(String nmea) { 161 | 162 | if (callback != null) 163 | callback.onNMEA(nmea); 164 | } 165 | 166 | private static void onMessage(String str) { 167 | 168 | if (callback != null) 169 | callback.onMessage(str); 170 | } 171 | 172 | public static void onStatus(String str) { 173 | 174 | if (callback != null) 175 | callback.onConsole(str); 176 | } 177 | 178 | public static void onError(String str) { 179 | 180 | if (callback != null) 181 | callback.onError(str); 182 | } 183 | 184 | public static void onUpdate() { 185 | 186 | if (callback != null) 187 | callback.onUpdate(); 188 | } 189 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jvdegithub/aiscatcher/AisService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * AIS-catcher for Android 3 | * Copyright (C) 2022-2023 jvde.github@gmail.com. 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 com.jvdegithub.aiscatcher; 20 | 21 | import android.app.ActivityManager; 22 | import android.app.Notification; 23 | import android.app.NotificationChannel; 24 | import android.app.NotificationManager; 25 | import android.app.PendingIntent; 26 | import android.app.Service; 27 | import android.content.Context; 28 | import android.content.Intent; 29 | import android.os.Build; 30 | import android.os.IBinder; 31 | import android.os.PowerManager; 32 | 33 | import androidx.localbroadcastmanager.content.LocalBroadcastManager; 34 | 35 | public class AisService extends Service { 36 | 37 | PowerManager.WakeLock wakeLock; 38 | 39 | public interface ServiceCallback { 40 | void onClose(); 41 | } 42 | 43 | private void sendBroadcast (){ 44 | Intent intent = new Intent ("message"); //put the same message as in the filter you used in the activity when registering the receiver 45 | LocalBroadcastManager.getInstance(this).sendBroadcast(intent); 46 | } 47 | 48 | public static boolean isRunning(Context context) { 49 | ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 50 | 51 | for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { 52 | if (AisService.class.getName().equals(service.service.getClassName())) { 53 | if (service.foreground) { 54 | return true; 55 | } 56 | } 57 | } 58 | 59 | return false; 60 | } 61 | 62 | private Notification buildNotification(String msg) { 63 | Notification.Builder notification; 64 | 65 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 66 | final String CHANNELID = "Foreground Service AIS-catcher"; 67 | NotificationChannel channel = new NotificationChannel(CHANNELID, CHANNELID, 68 | NotificationManager.IMPORTANCE_HIGH); 69 | 70 | getSystemService(NotificationManager.class).createNotificationChannel(channel); 71 | notification = new Notification.Builder(this, CHANNELID); 72 | } else { 73 | notification = new Notification.Builder(this); 74 | } 75 | 76 | Intent notificationIntent = new Intent(this.getApplicationContext(), MainActivity.class); 77 | int f = 0; 78 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) 79 | f = PendingIntent.FLAG_MUTABLE; 80 | PendingIntent contentIntent = PendingIntent.getActivity(this.getApplicationContext(), 0, notificationIntent, f); 81 | 82 | notification.setContentIntent(contentIntent) 83 | .setContentText(msg) 84 | .setContentTitle("AIS-catcher") 85 | .setSmallIcon(R.drawable.ic_notif_launcher); 86 | 87 | return notification.build(); 88 | } 89 | 90 | 91 | public void acquireLocks() { 92 | PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); 93 | if (powerManager != null) { 94 | wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AIS-catcher:WakeLock"); 95 | if (wakeLock != null && !wakeLock.isHeld()) { 96 | wakeLock.acquire(); 97 | } 98 | } 99 | } 100 | 101 | public void releaseLocks() { 102 | if (wakeLock != null && wakeLock.isHeld()) { 103 | wakeLock.release(); 104 | } 105 | } 106 | 107 | 108 | @Override 109 | public int onStartCommand(Intent intent, int flags, int startId) { 110 | 111 | if(intent != null) { 112 | 113 | int source = (int) intent.getExtras().get("source"); 114 | int fd = (int) intent.getExtras().get("USB"); 115 | int cgfwide = (int) intent.getExtras().get("CGFWIDE"); 116 | int modeltype = (int) intent.getExtras().get("MODELTYPE"); 117 | int FPDS = (int) intent.getExtras().get("FPDS"); 118 | 119 | int r = AisCatcherJava.createReceiver(source, fd, cgfwide, modeltype, FPDS); 120 | 121 | if (r == 0) { 122 | String msg = "Receiver running - " + DeviceManager.getDeviceTypeDescription() + " @ " + AisCatcherJava.getRateDescription(); 123 | startForeground(1001, buildNotification(msg)); 124 | 125 | new Thread(() -> { 126 | try { 127 | acquireLocks(); 128 | AisCatcherJava.Run(); 129 | AisCatcherJava.Close(); 130 | sendBroadcast(); 131 | 132 | } finally { 133 | releaseLocks(); 134 | 135 | stopForeground(true); 136 | stopSelf(); 137 | } 138 | }).start(); 139 | } else { 140 | String msg = "Receiver creation failed"; 141 | startForeground(1001, buildNotification(msg)); 142 | 143 | stopForeground(true); 144 | stopSelf(); 145 | sendBroadcast(); 146 | } 147 | } 148 | return super.onStartCommand(intent, flags, startId); 149 | } 150 | 151 | @Override 152 | public IBinder onBind(Intent intent) { 153 | throw new UnsupportedOperationException("Not yet implemented"); 154 | } 155 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jvdegithub/aiscatcher/DeviceManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * AIS-catcher for Android 3 | * Copyright (C) 2022-2023 jvde.github@gmail.com. 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 com.jvdegithub.aiscatcher; 20 | 21 | import android.app.PendingIntent; 22 | import android.content.BroadcastReceiver; 23 | import android.content.Context; 24 | import android.content.Intent; 25 | import android.content.IntentFilter; 26 | import android.content.res.XmlResourceParser; 27 | import android.hardware.usb.UsbDevice; 28 | import android.hardware.usb.UsbDeviceConnection; 29 | import android.hardware.usb.UsbManager; 30 | import android.os.Build; 31 | import android.util.AttributeSet; 32 | import android.util.Xml; 33 | import androidx.core.content.ContextCompat; 34 | import androidx.core.util.Pair; 35 | 36 | import org.xmlpull.v1.XmlPullParser; 37 | 38 | import java.util.ArrayList; 39 | import java.util.HashSet; 40 | 41 | 42 | public class DeviceManager { 43 | 44 | static Context context = null; 45 | private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"; 46 | 47 | enum DeviceType {NONE, RTLTCP, RTLSDR, AIRSPY, AIRSPYHF, HACKRF, SPYSERVER } 48 | 49 | public interface DeviceCallback { 50 | 51 | void onSourceChange(); 52 | } 53 | 54 | private static DeviceManager.DeviceCallback callback = null; 55 | 56 | static class Device { 57 | 58 | private final UsbDevice device; 59 | private final int UID; 60 | private final String description; 61 | private final DeviceType type; 62 | 63 | Device(UsbDevice d, String e, DeviceType t, int u) { 64 | device = d; 65 | description = e; 66 | type = t; 67 | UID = u; 68 | } 69 | 70 | String getDescription() { 71 | return description; 72 | } 73 | 74 | int getUID() { 75 | return UID; 76 | } 77 | 78 | UsbDevice getDevice() { 79 | return device; 80 | } 81 | 82 | DeviceType getType() { 83 | return type; 84 | } 85 | } 86 | 87 | private static final ArrayList devices = new ArrayList<>(); 88 | 89 | static int deviceIndex = 0; 90 | static int deviceUID = 0; 91 | static DeviceType deviceType = DeviceType.NONE; 92 | 93 | static UsbDeviceConnection usbDeviceConnection = null; 94 | 95 | public static void register(DeviceCallback cb) { 96 | callback = cb; 97 | registerUSBBroadCast(); 98 | } 99 | 100 | public static void unregister() { 101 | unregisterUSBBroadCast(); 102 | callback = null; 103 | } 104 | 105 | public static void onSourceChanged() { 106 | 107 | if (callback != null) 108 | callback.onSourceChange(); 109 | } 110 | 111 | public static int openDevice() { 112 | 113 | int fd = 0; 114 | 115 | AisCatcherJava.onStatus("Opening Device Connection\n"); 116 | 117 | if (devices.get(deviceIndex).type != DeviceType.RTLTCP && devices.get(deviceIndex).type != DeviceType.SPYSERVER) { 118 | 119 | try { 120 | UsbManager mUsbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); 121 | 122 | if(mUsbManager.hasPermission(devices.get(deviceIndex).getDevice())) { 123 | UsbDeviceConnection conn = mUsbManager.openDevice(devices.get(deviceIndex).getDevice()); 124 | fd = conn.getFileDescriptor(); 125 | AisCatcherJava.onStatus("Device SN: " + conn.getSerial() + ", FD: " + fd + "\n"); 126 | } 127 | else 128 | { 129 | AisCatcherJava.onStatus("No permission to USB device\n"); 130 | int f = 0; 131 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) 132 | f = PendingIntent.FLAG_MUTABLE; 133 | 134 | PendingIntent permissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), f); 135 | mUsbManager.requestPermission(devices.get(deviceIndex).getDevice(),permissionIntent); 136 | AisCatcherJava.onStatus("Permission requested\n"); 137 | return -1; 138 | 139 | } 140 | } catch (Exception e){ 141 | AisCatcherJava.onStatus(e.toString()); 142 | return -1; 143 | } 144 | } 145 | return fd; 146 | } 147 | 148 | public static DeviceType getDeviceType() { 149 | return deviceType; 150 | } 151 | 152 | public static String getDeviceTypeDescription() { 153 | 154 | switch (deviceType) { 155 | case RTLTCP: 156 | return "TCP"; 157 | case RTLSDR: 158 | return "RTL-SDR"; 159 | case AIRSPY: 160 | return "AirSpy"; 161 | case AIRSPYHF: 162 | return "AirSpy HF+"; 163 | case SPYSERVER: 164 | return "SpyServer"; 165 | } 166 | return "Unknown"; 167 | } 168 | public static int getDeviceCode() { 169 | switch (deviceType) { 170 | case RTLTCP: 171 | return 0; 172 | case RTLSDR: 173 | return 1; 174 | case AIRSPY: 175 | return 2; 176 | case AIRSPYHF: 177 | return 3; 178 | case SPYSERVER: 179 | return 4; 180 | } 181 | return 0; 182 | } 183 | 184 | public static void closeDevice() { 185 | 186 | AisCatcherJava.onStatus("Closing connection\n"); 187 | 188 | if (devices.get(deviceIndex).getType() != DeviceType.RTLTCP && devices.get(deviceIndex).getType() != DeviceType.SPYSERVER && usbDeviceConnection != null) { 189 | usbDeviceConnection.close(); 190 | } 191 | usbDeviceConnection = null; 192 | } 193 | 194 | public static void Init(Context m) { 195 | context = m; 196 | refreshList(false); 197 | } 198 | 199 | private static HashSet> getSupportedDevices() { 200 | HashSet> pairSet = new HashSet<>(); 201 | try { 202 | final XmlResourceParser xml = context.getResources().getXml(R.xml.usb_device_filter); 203 | 204 | xml.next(); 205 | int eventType; 206 | while ((eventType = xml.getEventType()) != XmlPullParser.END_DOCUMENT) { 207 | 208 | switch (eventType) { 209 | case XmlPullParser.START_TAG: 210 | if (xml.getName().equals("usb-device")) { 211 | final AttributeSet as = Xml.asAttributeSet(xml); 212 | final Integer vendorId = Integer.valueOf( as.getAttributeValue(null, "vendor-id")); 213 | final Integer productId = Integer.valueOf( as.getAttributeValue(null, "product-id")); 214 | pairSet.add(new Pair<>(vendorId, productId)); 215 | } 216 | break; 217 | } 218 | xml.next(); 219 | } 220 | } catch (Exception e) { 221 | e.printStackTrace(); 222 | } 223 | 224 | return pairSet; 225 | } 226 | 227 | private static boolean refreshList(boolean add) { 228 | 229 | devices.clear(); 230 | 231 | devices.add(new Device(null, "SpyServer", DeviceType.SPYSERVER, 0)); 232 | devices.add(new Device(null, "TCP", DeviceType.RTLTCP, 0)); 233 | 234 | final HashSet> supported = getSupportedDevices(); 235 | UsbManager mUsbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); 236 | 237 | for (UsbDevice device : mUsbManager.getDeviceList().values()) { 238 | 239 | if (supported.contains(new Pair<>(device.getVendorId(), device.getProductId()))) { 240 | 241 | Device dev; 242 | if (device.getVendorId() == 7504 && device.getProductId() == 24737) 243 | dev = new Device(device, "Airspy", DeviceType.AIRSPY, device.getDeviceId()); 244 | else if (device.getVendorId() == 7504 && device.getProductId() == 24713) 245 | dev = new Device(device, "HackRF", DeviceType.HACKRF, device.getDeviceId()); 246 | else if (device.getVendorId() == 1003 && device.getProductId() == 32780) 247 | dev = new Device(device, "Airspy HF+", DeviceType.AIRSPYHF, device.getDeviceId()); 248 | else 249 | dev = new Device(device, "RTL-SDR", DeviceType.RTLSDR, device.getDeviceId()); 250 | 251 | devices.add(dev); 252 | } 253 | else 254 | { 255 | AisCatcherJava.onStatus("Warning: not supported USB devices connected - VID: " + device.getVendorId() + " PID " + device.getProductId() + "\n"); 256 | } 257 | } 258 | 259 | int nDev = devices.size(); 260 | int select = nDev - 1; 261 | boolean changed = true; 262 | 263 | if (!add && (deviceType != DeviceType.RTLTCP && deviceType != DeviceType.SPYSERVER)) 264 | //if (!(add && deviceType == DeviceType.RTLTCP && deviceType == DeviceType.SPYSERVER)) 265 | for (int i = 0; i < devices.size(); i++) 266 | if (devices.get(i).getType() == deviceType && devices.get(i).getUID() == deviceUID) { 267 | select = i; 268 | changed = false; 269 | } 270 | 271 | setDevice(select); 272 | 273 | if (changed) onSourceChanged(); 274 | return changed; 275 | } 276 | 277 | public static void setDevice(int select) { 278 | if(select>=devices.size() || select < 0) 279 | select = devices.size()-1; 280 | 281 | deviceIndex = select; 282 | Device dev = devices.get(select); 283 | deviceType = dev.getType(); 284 | deviceUID = dev.getUID(); 285 | 286 | AisCatcherJava.setDeviceDescription(dev.getDescription(),"",""); 287 | 288 | onSourceChanged(); 289 | } 290 | public static String[] getDeviceStrings() { 291 | 292 | refreshList(false); 293 | 294 | String[] devs = new String[devices.size()]; 295 | int idx = 0; 296 | 297 | for (Device dev : devices) { 298 | 299 | devs[idx] = (idx + 1) + ": " + dev.description; 300 | idx++; 301 | } 302 | 303 | return devs; 304 | } 305 | /* 306 | public static void registerUSBBroadCast() { 307 | IntentFilter filter = new IntentFilter(); 308 | filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); 309 | filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); 310 | filter.addAction(ACTION_USB_PERMISSION); 311 | 312 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 313 | context.registerReceiver(mUsbReceiver, filter, Context.RECEIVER_NOT_EXPORTED); 314 | } else { 315 | context.registerReceiver(mUsbReceiver, filter); 316 | } 317 | } 318 | */ 319 | public static void registerUSBBroadCast() { 320 | IntentFilter filter = new IntentFilter(); 321 | filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); 322 | filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); 323 | filter.addAction(ACTION_USB_PERMISSION); 324 | 325 | ContextCompat.registerReceiver(context, mUsbReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED); 326 | } 327 | 328 | public static void unregisterUSBBroadCast() { 329 | context.unregisterReceiver(mUsbReceiver); 330 | } 331 | 332 | static BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { 333 | 334 | public void onReceive(Context context, Intent intent) { 335 | 336 | boolean add = false; 337 | 338 | String action = intent.getAction(); 339 | String action_clean = ""; 340 | 341 | if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { 342 | action_clean = "USB device connected"; 343 | add = true; 344 | } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { 345 | action_clean = "USB device disconnected"; 346 | } else if (ACTION_USB_PERMISSION.equals(action)) { 347 | action_clean = "USB device granted extra permission. Try to start again."; 348 | } 349 | 350 | AisCatcherJava.onStatus("Android: " + action_clean + ".\n"); 351 | refreshList(add); 352 | } 353 | }; 354 | } 355 | -------------------------------------------------------------------------------- /app/src/main/java/com/jvdegithub/aiscatcher/LocationHelper.java: -------------------------------------------------------------------------------- 1 | package com.jvdegithub.aiscatcher; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.pm.PackageManager; 7 | import android.location.Location; 8 | import android.location.LocationListener; 9 | import android.location.LocationManager; 10 | import android.os.Bundle; 11 | import android.util.Log; 12 | import android.widget.Toast; 13 | 14 | import androidx.annotation.NonNull; 15 | import androidx.core.app.ActivityCompat; 16 | import androidx.core.content.ContextCompat; 17 | 18 | public class LocationHelper implements LocationListener { 19 | 20 | public static final int PERMISSION_REQUEST_CODE = 1; 21 | private Context context; 22 | private LocationManager locationManager; 23 | private boolean isUpdatingLocation = false; 24 | public LocationHelper(Context context) { 25 | this.context = context; 26 | locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); 27 | } 28 | 29 | public void requestLocationUpdates() { 30 | if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED 31 | || ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { 32 | 33 | ActivityCompat.requestPermissions((Activity) context, 34 | new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, 35 | PERMISSION_REQUEST_CODE); 36 | } else { 37 | if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { 38 | locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 30 * 1000, 0, this); 39 | isUpdatingLocation = true; 40 | } else if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { 41 | locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 30 * 1000, 0, this); 42 | isUpdatingLocation = true; 43 | 44 | Toast.makeText(context, "GPS not available. Using network for location updates.", Toast.LENGTH_LONG).show(); 45 | } else { 46 | } 47 | } 48 | } 49 | 50 | 51 | @Override 52 | public void onLocationChanged(Location location) { 53 | Log.d("Location Update", String.format("New Location Received: (Latitude: %s, Longitude: %s)", 54 | location.getLatitude(), location.getLongitude())); 55 | AisCatcherJava.setLatLon((float) location.getLatitude(), (float) location.getLongitude()); 56 | } 57 | 58 | 59 | public void removeLocationUpdates() { 60 | if (isUpdatingLocation) { 61 | locationManager.removeUpdates(this); 62 | isUpdatingLocation = false; 63 | } 64 | } 65 | @Override 66 | public void onStatusChanged(String provider, int status, Bundle extras) { } 67 | 68 | @Override 69 | public void onProviderEnabled(String provider) { } 70 | 71 | @Override 72 | public void onProviderDisabled(String provider) { } 73 | 74 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 75 | if (requestCode == PERMISSION_REQUEST_CODE) { 76 | if (grantResults.length > 0 77 | && grantResults[0] == PackageManager.PERMISSION_GRANTED 78 | && grantResults[1] == PackageManager.PERMISSION_GRANTED) { 79 | 80 | requestLocationUpdates(); 81 | } else { 82 | // no permission 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jvdegithub/aiscatcher/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * AIS-catcher for Android 3 | * Copyright (C) 2022-2023 jvde.github@gmail.com. 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 com.jvdegithub.aiscatcher; 20 | 21 | import static android.Manifest.permission.POST_NOTIFICATIONS; 22 | 23 | import android.Manifest; 24 | import android.content.BroadcastReceiver; 25 | import android.content.Context; 26 | import android.content.Intent; 27 | import android.content.IntentFilter; 28 | import android.content.SharedPreferences; 29 | import android.content.pm.PackageInfo; 30 | import android.content.pm.PackageManager; 31 | 32 | import android.content.res.Configuration; 33 | import android.graphics.Color; 34 | import android.graphics.PorterDuff; 35 | import android.graphics.drawable.Drawable; 36 | import android.net.Uri; 37 | import android.os.Build; 38 | import android.os.Bundle; 39 | import android.text.Html; 40 | import android.text.Spanned; 41 | import android.util.Log; 42 | import android.view.Menu; 43 | import android.view.MenuItem; 44 | import android.webkit.WebView; 45 | import android.widget.FrameLayout; 46 | import android.widget.Toast; 47 | 48 | import androidx.annotation.NonNull; 49 | import androidx.annotation.Nullable; 50 | import androidx.appcompat.app.AlertDialog; 51 | import androidx.appcompat.app.AppCompatActivity; 52 | import androidx.appcompat.app.AppCompatDelegate; 53 | import androidx.core.app.ActivityCompat; 54 | import androidx.core.content.ContextCompat; 55 | import androidx.fragment.app.Fragment; 56 | import androidx.localbroadcastmanager.content.LocalBroadcastManager; 57 | import androidx.preference.PreferenceManager; 58 | import androidx.webkit.WebViewCompat; 59 | 60 | import com.google.android.material.bottomnavigation.BottomNavigationView; 61 | import com.jvdegithub.aiscatcher.databinding.ActivityMainBinding; 62 | import com.jvdegithub.aiscatcher.tools.LogBook; 63 | import com.jvdegithub.aiscatcher.ui.main.StatisticsFragment; 64 | import com.jvdegithub.aiscatcher.ui.main.WebViewMapFragment; 65 | 66 | import java.io.IOException; 67 | import java.net.ServerSocket; 68 | 69 | public class MainActivity extends AppCompatActivity implements AisCatcherJava.AisCallback, DeviceManager.DeviceCallback { 70 | 71 | private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; 72 | private boolean firstRun = true; 73 | 74 | private LogBook logbook; 75 | private LocationHelper locationHelper; 76 | public static int port = 0; 77 | static { 78 | 79 | try { 80 | ServerSocket serverSocket = new ServerSocket(port); 81 | port = serverSocket.getLocalPort(); 82 | serverSocket.close(); 83 | } catch (IOException e) { 84 | e.printStackTrace(); 85 | } 86 | 87 | System.loadLibrary("AIScatcherNDK"); 88 | AisCatcherJava.Init(port); 89 | } 90 | 91 | boolean legacyVersion = true; 92 | 93 | private StatisticsFragment stat_fragment; 94 | private BottomNavigationView bottomNavigationView; 95 | 96 | @Override 97 | protected void onCreate(Bundle savedInstanceState) { 98 | super.onCreate(savedInstanceState); 99 | 100 | /* ask for permission to post notifications if needed ..... */ 101 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 102 | if (ContextCompat.checkSelfPermission(this, POST_NOTIFICATIONS) == PackageManager.PERMISSION_DENIED) { 103 | ActivityCompat.requestPermissions(this, new String[]{POST_NOTIFICATIONS}, 101); 104 | } 105 | } 106 | 107 | com.jvdegithub.aiscatcher.databinding.ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater()); 108 | setContentView(binding.getRoot()); 109 | setSupportActionBar(binding.toolbar); 110 | 111 | setDarkMode(true); 112 | 113 | logbook = LogBook.getInstance(); 114 | 115 | FrameLayout fragmentContainer = findViewById(R.id.fragment_container); 116 | 117 | int currentApiVersion = android.os.Build.VERSION.SDK_INT; 118 | legacyVersion = shouldUseLegacyMode(); 119 | 120 | Fragment fragment; 121 | 122 | if (legacyVersion) { 123 | stat_fragment = new StatisticsFragment(); 124 | fragment = stat_fragment; 125 | } else { 126 | fragment = new WebViewMapFragment(); 127 | } 128 | 129 | getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment).commit(); 130 | 131 | if(Settings.setDefaultOnFirst(this)) { 132 | onOpening(); 133 | } 134 | 135 | locationHelper = new LocationHelper(this); 136 | locationHelper.requestLocationUpdates(); 137 | 138 | bottomNavigationView = binding.bottombar; 139 | bottomNavigationView.setOnItemSelectedListener(item -> { 140 | switch (item.getItemId()) { 141 | case R.id.action_play: 142 | onPlayStop(); 143 | return true; 144 | case R.id.action_clear: 145 | onClear(); 146 | return true; 147 | case R.id.action_source: 148 | onSource(); 149 | return true; 150 | case R.id.action_web: 151 | onWeb(); 152 | return true; 153 | } 154 | return false; 155 | }); 156 | 157 | DeviceManager.Init(this); 158 | 159 | preferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { 160 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 161 | if (key.equals("sFORCEDARK")) 162 | setDarkMode(false); 163 | else if (key.equals("sKEEPSCREENON")) 164 | applyKeepScreenOnSetting(); 165 | } 166 | }; 167 | 168 | SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 169 | sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener); 170 | } 171 | 172 | // ugly to have the callback in mainactivity, to be cleaned up 173 | @Override 174 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 175 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 176 | if (requestCode == locationHelper.PERMISSION_REQUEST_CODE) { 177 | if (grantResults.length > 0 178 | && grantResults[0] == PackageManager.PERMISSION_GRANTED 179 | && grantResults[1] == PackageManager.PERMISSION_GRANTED) { 180 | 181 | locationHelper.requestLocationUpdates(); 182 | } 183 | } 184 | } 185 | 186 | @Override 187 | protected void onDestroy () { 188 | super.onDestroy(); 189 | locationHelper.removeLocationUpdates(); 190 | PreferenceManager.getDefaultSharedPreferences(this) 191 | .unregisterOnSharedPreferenceChangeListener(preferenceChangeListener); 192 | 193 | } 194 | 195 | public void setDarkMode(boolean only_force) { 196 | SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 197 | boolean sForceDark = sharedPreferences.getBoolean("sFORCEDARK", false); 198 | if(!only_force || sForceDark) 199 | AppCompatDelegate.setDefaultNightMode(sForceDark ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); 200 | } 201 | 202 | private void onWeb () { 203 | 204 | Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://aiscatcher.org")); 205 | startActivity(browserIntent); 206 | } 207 | 208 | protected void AutoStart() { 209 | if(!Settings.getAutoStart(this)) return; 210 | if(DeviceManager.getDeviceType() != DeviceManager.DeviceType.RTLSDR && 211 | DeviceManager.getDeviceType() != DeviceManager.DeviceType.AIRSPY && 212 | DeviceManager.getDeviceType() != DeviceManager.DeviceType.AIRSPYHF) 213 | return; 214 | 215 | if (!AisService.isRunning(getApplicationContext())) { 216 | onPlayStop(); 217 | } 218 | } 219 | protected void onResume () { 220 | 221 | super.onResume(); 222 | 223 | applyKeepScreenOnSetting(); 224 | updateUIonSource(); 225 | if (AisService.isRunning(getApplicationContext())) { 226 | updateUIwithStart(); 227 | } else { 228 | updateUIwithStop(); 229 | } 230 | 231 | if(firstRun) { 232 | firstRun = false; 233 | AutoStart(); 234 | } 235 | } 236 | 237 | private void applyKeepScreenOnSetting() { 238 | boolean keepScreenOn = Settings.getKeepScreenOn(this); 239 | if (keepScreenOn) { 240 | getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 241 | } else { 242 | getWindow().clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 243 | } 244 | } 245 | 246 | @Override 247 | protected void onStart () { 248 | super.onStart(); 249 | 250 | AisCatcherJava.registerCallback(this); 251 | DeviceManager.register(this); 252 | LocalBroadcastManager.getInstance(this).registerReceiver(bReceiver, new IntentFilter("message")); 253 | } 254 | 255 | @Override 256 | protected void onStop () { 257 | super.onStop(); 258 | 259 | AisCatcherJava.unregisterCallback(); 260 | DeviceManager.unregister(); 261 | LocalBroadcastManager.getInstance(this).unregisterReceiver(bReceiver); 262 | } 263 | 264 | private void onPlayStop () { 265 | if (!AisService.isRunning(getApplicationContext())) { 266 | if (Settings.Apply(this)) { 267 | int fd = DeviceManager.openDevice(); 268 | if (fd != -1) { 269 | Intent serviceIntent = new Intent(MainActivity.this, AisService.class); 270 | serviceIntent.putExtra("source", DeviceManager.getDeviceCode()); 271 | serviceIntent.putExtra("USB", fd); 272 | serviceIntent.putExtra("CGFWIDE", Settings.getCGFSetting(this)); 273 | serviceIntent.putExtra("MODELTYPE", Settings.getModelType(this)); 274 | serviceIntent.putExtra("FPDS", Settings.getFixedPointDownsampling(this) ? 1 : 0); 275 | serviceIntent.putExtra("USB", fd); 276 | ContextCompat.startForegroundService(MainActivity.this, serviceIntent); 277 | updateUIwithStart(); 278 | } else 279 | Toast.makeText(MainActivity.this, "Cannot open USB device. Give permission first and try again.", Toast.LENGTH_LONG).show(); 280 | } else 281 | Toast.makeText(MainActivity.this, "Invalid setting", Toast.LENGTH_LONG).show(); 282 | 283 | } else { 284 | AisCatcherJava.forceStop(); 285 | } 286 | } 287 | 288 | private BroadcastReceiver bReceiver = new BroadcastReceiver() { 289 | 290 | @Override 291 | public void onReceive(Context context, Intent intent) { 292 | onAisServiceClosing(); 293 | } 294 | }; 295 | 296 | @Override 297 | public boolean onOptionsItemSelected (MenuItem item){ 298 | switch (item.getItemId()) { 299 | case R.id.action_settings: 300 | Intent myIntent = new Intent(MainActivity.this, Settings.class); 301 | MainActivity.this.startActivity(myIntent); 302 | return true; 303 | case R.id.action_default: 304 | Settings.setDefault(this); 305 | return true; 306 | case R.id.action_credit: 307 | onCredit(); 308 | return true; 309 | case R.id.action_abouts: 310 | onAbout(); 311 | return true; 312 | case R.id.action_logs: 313 | onLogs(); 314 | return true; 315 | } 316 | return super.onOptionsItemSelected(item); 317 | 318 | } 319 | private void onCredit () { 320 | 321 | Spanned html = Html.fromHtml((String) getText(R.string.license_text)); 322 | showDialog(html, "Licenses"); 323 | } 324 | 325 | private void onLogs () { 326 | 327 | Spanned html = Html.fromHtml(logbook.getLogAsString()); 328 | showDialog(html, "Log"); 329 | } 330 | 331 | private void onAbout () { 332 | String disclaimerText = getString(R.string.disclaimer_text); 333 | String androidVersion = String.format(Build.VERSION.RELEASE); 334 | String versionName = String.valueOf(BuildConfig.VERSION_NAME); 335 | String versionCode = String.valueOf(BuildConfig.VERSION_CODE); 336 | String libraryVersion = AisCatcherJava.getLibraryVersion(); 337 | String appID = String.valueOf(BuildConfig.APPLICATION_ID); 338 | String debug = String.valueOf(BuildConfig.BUILD_TYPE); 339 | WebView webView = new WebView(this); 340 | String userAgentString = webView.getSettings().getUserAgentString(); 341 | 342 | String formattedHtml = String.format(disclaimerText,versionName,versionCode, 343 | libraryVersion,appID,debug,androidVersion, userAgentString); 344 | Spanned html = Html.fromHtml(formattedHtml); 345 | 346 | showDialog(html, "About"); 347 | } 348 | 349 | private void onOpening () { 350 | onAbout(); 351 | } 352 | 353 | private void showDialog (@Nullable CharSequence msg, String title) 354 | { 355 | int width = (int) (getResources().getDisplayMetrics().widthPixels * 0.90); 356 | int height = (int) (getResources().getDisplayMetrics().heightPixels * 0.90); 357 | 358 | AlertDialog.Builder builder = new AlertDialog.Builder(this); 359 | builder.setTitle(title); 360 | builder.setMessage(msg); 361 | builder.setCancelable(true); 362 | builder.setPositiveButton("OK", null); 363 | AlertDialog alertDialog = builder.create(); 364 | alertDialog.show(); 365 | alertDialog.getWindow().setLayout(width, height); //Controlling width and height. 366 | } 367 | private void onClear () { 368 | AisCatcherJava.Reset(); 369 | } 370 | 371 | private void onSource () { 372 | 373 | AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); 374 | String[] devs = DeviceManager.getDeviceStrings(); 375 | 376 | builder.setTitle("Select Device"); 377 | builder.setItems(devs, (dialog, select) -> DeviceManager.setDevice(select)); 378 | builder.show(); 379 | } 380 | 381 | private void updateUIwithStart () { 382 | MenuItem item = bottomNavigationView.getMenu().findItem(R.id.action_play); 383 | 384 | item.setIcon(R.drawable.ic_baseline_stop_circle_40); 385 | item.setTitle("Stop"); 386 | bottomNavigationView.getMenu().findItem(R.id.action_source).setEnabled(false); 387 | Settings.setEnabled(false); 388 | } 389 | 390 | private void updateUIwithStop () { 391 | MenuItem item = bottomNavigationView.getMenu().findItem(R.id.action_play); 392 | item.setIcon(R.drawable.ic_baseline_play_circle_filled_40); 393 | item.setTitle("Start"); 394 | bottomNavigationView.getMenu().findItem(R.id.action_source).setEnabled(true); 395 | Settings.setEnabled(true); 396 | } 397 | 398 | private void updateUIonSource () { 399 | MenuItem item = bottomNavigationView.getMenu().findItem(R.id.action_source); 400 | item.setTitle(DeviceManager.getDeviceTypeDescription()); 401 | } 402 | 403 | @Override 404 | public boolean onCreateOptionsMenu (Menu menu){ 405 | 406 | getMenuInflater().inflate(R.menu.toolbar_menu, menu); 407 | return true; 408 | } 409 | 410 | @Override 411 | public void onConsole ( final String line){ 412 | Log.d("AIS-catcher library",line); 413 | logbook.addLog(line); 414 | } 415 | 416 | @Override 417 | public void onNMEA ( final String line){ 418 | //nmea_fragment.Update(line); 419 | } 420 | 421 | @Override 422 | public void onMessage ( final String line){ 423 | //map_fragment.Update(line); 424 | } 425 | 426 | @Override 427 | public void onError ( final String line){ 428 | runOnUiThread(() -> Toast.makeText(MainActivity.this, line, Toast.LENGTH_LONG).show()); 429 | } 430 | 431 | public void onAisServiceClosing () { 432 | DeviceManager.closeDevice(); 433 | runOnUiThread(this::updateUIwithStop); 434 | } 435 | 436 | @Override 437 | public void onUpdate () { 438 | if (legacyVersion) 439 | stat_fragment.Update(); 440 | } 441 | 442 | @Override 443 | public void onSourceChange () { 444 | updateUIonSource(); 445 | } 446 | 447 | 448 | private boolean shouldUseLegacyMode() { 449 | // Original API level check 450 | int currentApiVersion = android.os.Build.VERSION.SDK_INT; 451 | if (currentApiVersion < Build.VERSION_CODES.Q) { 452 | return true; 453 | } 454 | 455 | return false; 456 | } 457 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jvdegithub/aiscatcher/Settings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * AIS-catcher for Android 3 | * Copyright (C) 2022-2023 jvde.github@gmail.com. 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 com.jvdegithub.aiscatcher; 20 | 21 | import android.content.Context; 22 | import android.content.SharedPreferences; 23 | import android.os.Bundle; 24 | import android.text.InputFilter; 25 | import android.text.InputType; 26 | import android.text.method.DigitsKeyListener; 27 | import android.util.Log; 28 | import android.widget.Toast; 29 | 30 | import androidx.appcompat.app.AppCompatActivity; 31 | import androidx.preference.EditTextPreference; 32 | import androidx.preference.ListPreference; 33 | import androidx.preference.Preference; 34 | import androidx.preference.PreferenceFragmentCompat; 35 | import androidx.preference.PreferenceManager; 36 | import androidx.preference.PreferenceScreen; 37 | import androidx.preference.SeekBarPreference; 38 | 39 | import com.jvdegithub.aiscatcher.tools.InputFilterIP; 40 | import com.jvdegithub.aiscatcher.tools.InputFilterMinMax; 41 | 42 | import java.util.Objects; 43 | 44 | public class Settings extends AppCompatActivity { 45 | 46 | static boolean is_enabled = true; 47 | public static void setEnabled(boolean e) { 48 | is_enabled = e; 49 | } 50 | 51 | @Override 52 | protected void onCreate(Bundle savedInstanceState) { 53 | super.onCreate(savedInstanceState); 54 | getSupportFragmentManager().beginTransaction().replace(android.R.id.content, new SettingsFragment()).commit(); 55 | } 56 | 57 | static void setDefault(Context context) { 58 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 59 | 60 | preferences.edit().putString("sSHARINGKEY", "").commit(); 61 | preferences.edit().putBoolean("sSHARING", false).commit(); 62 | preferences.edit().putBoolean("sAUTOSTART", false).commit(); 63 | preferences.edit().putBoolean("sKEEPSCREENON", false).commit(); 64 | 65 | preferences.edit().putBoolean("w1SWITCH", false).commit(); 66 | preferences.edit().putString("w1PORT", "8100").commit(); 67 | 68 | preferences.edit().putString("oCGF_WIDE", "Default").commit(); 69 | preferences.edit().putString("oMODEL_TYPE", "Default").commit(); 70 | preferences.edit().putBoolean("oFP_DS", false).commit(); 71 | 72 | preferences.edit().putString("rRATE", "288K").commit(); 73 | preferences.edit().putBoolean("rRTLAGC", false).commit(); 74 | preferences.edit().putString("rTUNER", "Auto").commit(); 75 | preferences.edit().putBoolean("rBIASTEE", false).commit(); 76 | preferences.edit().putString("rFREQOFFSET", "0").commit(); 77 | preferences.edit().putBoolean("rBANDWIDTH", false).commit(); 78 | 79 | preferences.edit().putString("tPROTOCOL", "RTLTCP").commit(); 80 | preferences.edit().putString("tRATE", "240K").commit(); 81 | preferences.edit().putString("tTUNER", "Auto").commit(); 82 | preferences.edit().putString("tHOST", "localhost").commit(); 83 | preferences.edit().putString("tPORT", "12345").commit(); 84 | 85 | preferences.edit().putString("sRATE", "96K").commit(); 86 | preferences.edit().putString("sHOST", "localhost").commit(); 87 | preferences.edit().putString("sPORT", "5555").commit(); 88 | preferences.edit().putInt("sGAIN", 14).commit(); 89 | 90 | preferences.edit().putBoolean("u1SWITCH", true).commit(); 91 | preferences.edit().putString("u1HOST", "127.0.0.1").commit(); 92 | preferences.edit().putString("u1PORT", "10110").commit(); 93 | preferences.edit().putBoolean("u1JSON", false).commit(); 94 | 95 | preferences.edit().putBoolean("u2SWITCH", false).commit(); 96 | preferences.edit().putString("u2HOST", "127.0.0.1").commit(); 97 | preferences.edit().putString("u2PORT", "10111").commit(); 98 | preferences.edit().putBoolean("u2JSON", false).commit(); 99 | 100 | preferences.edit().putBoolean("u3SWITCH", false).commit(); 101 | preferences.edit().putString("u3HOST", "127.0.0.1").commit(); 102 | preferences.edit().putString("u3PORT", "10111").commit(); 103 | preferences.edit().putBoolean("u3JSON", false).commit(); 104 | 105 | preferences.edit().putBoolean("u4SWITCH", false).commit(); 106 | preferences.edit().putString("u4HOST", "127.0.0.1").commit(); 107 | preferences.edit().putString("u4PORT", "10111").commit(); 108 | preferences.edit().putBoolean("u4JSON", false).commit(); 109 | 110 | preferences.edit().putBoolean("s1SWITCH", false).commit(); 111 | preferences.edit().putString("s1PORT", "5012").commit(); 112 | 113 | preferences.edit().putInt("mLINEARITY", 17).commit(); 114 | preferences.edit().putString("mRATE", "2500K").commit(); 115 | preferences.edit().putBoolean("mBIASTEE", false).commit(); 116 | 117 | preferences.edit().putString("hRATE", "192K").commit(); 118 | } 119 | 120 | static boolean setDefaultOnFirst(Context context) { 121 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 122 | boolean pref_set = preferences.getBoolean("pref_set", false); 123 | if (!pref_set) setDefault(context); 124 | preferences.edit().putBoolean("pref_set", true).commit(); 125 | return !pref_set; 126 | } 127 | 128 | public static class SettingsFragment extends PreferenceFragmentCompat implements 129 | SharedPreferences.OnSharedPreferenceChangeListener { 130 | @Override 131 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 132 | // Load the preferences from an XML resource 133 | setPreferencesFromResource(R.xml.preferences, rootKey); 134 | 135 | ((EditTextPreference) getPreferenceManager().findPreference("sHOST")).setOnBindEditTextListener(validateIP); 136 | ((EditTextPreference) getPreferenceManager().findPreference("sPORT")).setOnBindEditTextListener(validatePort); 137 | ((EditTextPreference) getPreferenceManager().findPreference("w1PORT")).setOnBindEditTextListener(validatePort);; 138 | ((SeekBarPreference) getPreferenceManager().findPreference("sGAIN")).setUpdatesContinuously(true); 139 | ((EditTextPreference) getPreferenceManager().findPreference("tPORT")).setOnBindEditTextListener(validatePort); 140 | ((EditTextPreference) getPreferenceManager().findPreference("rFREQOFFSET")).setOnBindEditTextListener(validatePPM); 141 | ((EditTextPreference) getPreferenceManager().findPreference("tHOST")).setOnBindEditTextListener(validateIP); 142 | ((EditTextPreference) getPreferenceManager().findPreference("u1HOST")).setOnBindEditTextListener(validateIP); 143 | ((EditTextPreference) getPreferenceManager().findPreference("u2HOST")).setOnBindEditTextListener(validateIP); 144 | ((EditTextPreference) getPreferenceManager().findPreference("u3HOST")).setOnBindEditTextListener(validateIP); 145 | ((EditTextPreference) getPreferenceManager().findPreference("u4HOST")).setOnBindEditTextListener(validateIP); 146 | ((EditTextPreference) getPreferenceManager().findPreference("u1PORT")).setOnBindEditTextListener(validatePort); 147 | ((EditTextPreference) getPreferenceManager().findPreference("u2PORT")).setOnBindEditTextListener(validatePort); 148 | ((EditTextPreference) getPreferenceManager().findPreference("u3PORT")).setOnBindEditTextListener(validatePort); 149 | ((EditTextPreference) getPreferenceManager().findPreference("u4PORT")).setOnBindEditTextListener(validatePort); 150 | ((EditTextPreference) getPreferenceManager().findPreference("s1PORT")).setOnBindEditTextListener(validatePort); 151 | ((SeekBarPreference) getPreferenceManager().findPreference("mLINEARITY")).setUpdatesContinuously(true); 152 | 153 | setSummaries(); 154 | } 155 | 156 | static public int getModelType(Context context) 157 | { 158 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 159 | return preferences.getInt("oMODEL_TYPE", 0); 160 | } 161 | 162 | private void setSummaries() { 163 | setSummaryText(new String[]{"w1PORT","tPORT","tHOST","sPORT","sHOST","u1HOST","u1PORT","u2HOST","u2PORT", "u3HOST","u3PORT", "u4HOST","u4PORT", "s1PORT", "rFREQOFFSET", "sSHARINGKEY"}); 164 | setSummaryList(new String[]{"rTUNER","rRATE","sRATE","tRATE","tPROTOCOL","tTUNER","mRATE","hRATE","oMODEL_TYPE","oCGF_WIDE"}); 165 | setSummarySeekbar(new String[]{"mLINEARITY", "sGAIN"}); 166 | } 167 | 168 | private void setSummaryText(String[] settings) { 169 | 170 | for (String s : settings) { 171 | EditTextPreference e = findPreference(s); 172 | e.setSummary(e.getText()); 173 | } 174 | } 175 | 176 | private void setSummaryList(String[] settings) { 177 | for (String s : settings) { 178 | ListPreference e = findPreference(s); 179 | e.setSummary(e.getEntry()); 180 | } 181 | } 182 | 183 | private void setSummarySeekbar(String[] settings) { 184 | for(String s:settings) { 185 | SeekBarPreference e = findPreference(s); 186 | e.setSummary(String.valueOf(e.getValue())); 187 | } 188 | } 189 | 190 | @Override 191 | public void onResume() { 192 | super.onResume(); 193 | getPreferenceScreen().getSharedPreferences() 194 | .registerOnSharedPreferenceChangeListener(this); 195 | if(!is_enabled) 196 | Toast.makeText(getContext(), "Settings disabled during run", Toast.LENGTH_SHORT).show(); 197 | 198 | PreferenceScreen preferenceScreen = getPreferenceScreen(); 199 | for (int i = 0; i < preferenceScreen.getPreferenceCount(); i++) { 200 | Preference preference = preferenceScreen.getPreference(i); 201 | preference.setEnabled(is_enabled); 202 | } 203 | 204 | final Preference tPROTOCOL = findPreference("tPROTOCOL"); 205 | 206 | tPROTOCOL.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { 207 | @Override 208 | public boolean onPreferenceChange(Preference preference, Object newValue) { 209 | if ("TXT".equals(newValue.toString())) { 210 | findPreference("tRATE").setEnabled(false); 211 | findPreference("tTUNER").setEnabled(false); 212 | } else { 213 | findPreference("tRATE").setEnabled(is_enabled & true); 214 | findPreference("tTUNER").setEnabled(is_enabled & true); 215 | } 216 | return true; 217 | } 218 | }); 219 | 220 | String currentProtocolValue = tPROTOCOL.getSharedPreferences().getString("tPROTOCOL", ""); 221 | if ("TXT".equals(currentProtocolValue)) { 222 | findPreference("tRATE").setEnabled(false); 223 | findPreference("tTUNER").setEnabled(false); 224 | } else { 225 | findPreference("tRATE").setEnabled(is_enabled & true); 226 | findPreference("tTUNER").setEnabled(is_enabled & true); } 227 | } 228 | 229 | @Override 230 | public void onPause() { 231 | super.onPause(); 232 | getPreferenceScreen().getSharedPreferences() 233 | .unregisterOnSharedPreferenceChangeListener(this); 234 | 235 | Preference tPROTOCOL = findPreference("tPROTOCOL"); 236 | if (tPROTOCOL != null) { 237 | tPROTOCOL.setOnPreferenceChangeListener(null); // Remove the listener 238 | } 239 | } 240 | 241 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 242 | setSummaries(); 243 | } 244 | 245 | EditTextPreference.OnBindEditTextListener validatePPM = editText -> { 246 | editText.selectAll(); 247 | editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); 248 | editText.setFilters(new InputFilter[]{new InputFilterMinMax(-150,150)}); 249 | }; 250 | 251 | EditTextPreference.OnBindEditTextListener validatePort = editText -> { 252 | editText.selectAll(); 253 | editText.setInputType(InputType.TYPE_CLASS_NUMBER ); 254 | editText.setFilters(new InputFilter[]{new InputFilterMinMax(0,65536)}); 255 | }; 256 | 257 | EditTextPreference.OnBindEditTextListener validateInteger = editText -> { 258 | editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); 259 | editText.selectAll(); 260 | editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(5)}); 261 | }; 262 | 263 | EditTextPreference.OnBindEditTextListener validateIP = editText -> { 264 | editText.setKeyListener(DigitsKeyListener.getInstance("0123456789.")); 265 | editText.selectAll(); 266 | editText.setFilters(new InputFilter[]{new InputFilterIP()}); 267 | }; 268 | } 269 | 270 | static public boolean Apply(Context context) { 271 | 272 | if (!SetDevice(new String[]{"rRATE", "rTUNER", "rFREQOFFSET", "sRATE", "sPORT", "sHOST", "tRATE", "tPROTOCOL","tTUNER", "tHOST", "tPORT", "mRATE", "hRATE"}, context)) 273 | return false; 274 | if (!SetDeviceBoolean(new String[]{"rRTLAGC", "rBIASTEE", "mBIASTEE"}, "ON", "OFF", context)) 275 | return false; 276 | if (!SetDeviceInteger(new String[]{"mLINEARITY", "sGAIN"}, context)) return false; 277 | 278 | if(!SetRTLbandwidth(context)) return false; 279 | 280 | if (!SetUDPoutput("u1", context)) return false; 281 | if (!SetUDPoutput("u2", context)) return false; 282 | if (!SetUDPoutput("u3", context)) return false; 283 | if (!SetUDPoutput("u4", context)) return false; 284 | 285 | if (!SetTCPListener(context)) return false; 286 | 287 | if (!SetWebViewerOutput( context)) return false; 288 | 289 | if(!SetSharing(context)) return false; 290 | 291 | return true; 292 | } 293 | 294 | static public int getModelType(Context context) 295 | { 296 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 297 | String set = preferences.getString("oMODEL_TYPE", "Default"); 298 | 299 | if(set.equals("Default")) return 0; 300 | return 1; 301 | } 302 | 303 | static public int getCGFSetting(Context context) 304 | { 305 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 306 | String set = preferences.getString("oCGF_WIDE", "Default"); 307 | if(set.equals("Default")) return 1; 308 | if(set.equals("Narrow")) return 0; 309 | return 1; 310 | } 311 | 312 | static public boolean getAutoStart(Context context) 313 | { 314 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 315 | return preferences.getBoolean("sAUTOSTART",false); 316 | } 317 | 318 | static public boolean getKeepScreenOn(Context context) 319 | { 320 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 321 | return preferences.getBoolean("sKEEPSCREENON",false); 322 | } 323 | 324 | static public boolean getFixedPointDownsampling(Context context) 325 | { 326 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 327 | return preferences.getBoolean("oFP_DS", false); 328 | } 329 | 330 | static private boolean SetDevice(String[] settings, Context context) { 331 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 332 | 333 | for (String s : settings) { 334 | String p = preferences.getString(s, ""); 335 | if (Objects.equals(p, "")) return false; 336 | if (AisCatcherJava.applySetting(s.substring(0, 1), s.substring(1), p) != 0) 337 | return false; 338 | } 339 | return true; 340 | } 341 | 342 | static private boolean SetRTLbandwidth(Context context) 343 | { 344 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 345 | boolean b = preferences.getBoolean("rBANDWIDTH", false); 346 | if(b) { 347 | if (AisCatcherJava.applySetting("r", "BW", "192000") != 0) 348 | return false; 349 | } 350 | else { 351 | if (AisCatcherJava.applySetting("r", "BW", "0") != 0) 352 | return false; 353 | } 354 | return true; 355 | } 356 | 357 | static private boolean SetDeviceBoolean(String[] settings, String st, String sf, Context context) { 358 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 359 | 360 | for (String s : settings) { 361 | boolean b = preferences.getBoolean(s, true); 362 | if (AisCatcherJava.applySetting(s.substring(0, 1), s.substring(1), b ? st : sf) != 0) 363 | return false; 364 | } 365 | return true; 366 | } 367 | 368 | static private boolean SetDeviceInteger(String[] settings, Context context) { 369 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 370 | 371 | for (String s : settings) { 372 | String p = String.valueOf(preferences.getInt(s, 0)); 373 | if (Objects.equals(p, "")) return false; 374 | if (AisCatcherJava.applySetting(s.substring(0, 1), s.substring(1), p) != 0) 375 | return false; 376 | } 377 | return true; 378 | } 379 | 380 | static private boolean SetUDPoutput(String s, Context context) { 381 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 382 | 383 | boolean b = preferences.getBoolean(s + "SWITCH", true); 384 | if (b) { 385 | String host = preferences.getString(s + "HOST", ""); 386 | String port = preferences.getString(s + "PORT", ""); 387 | boolean JSON = preferences.getBoolean(s + "JSON", false); 388 | 389 | return AisCatcherJava.createUDP(host, port, JSON) == 0; 390 | 391 | } 392 | return true; 393 | } 394 | 395 | static private boolean SetTCPListener(Context context) { 396 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 397 | 398 | boolean b = preferences.getBoolean("s1SWITCH", true); 399 | if (b) { 400 | String port = preferences.getString("s1PORT", ""); 401 | 402 | return AisCatcherJava.createTCPlistener( port) == 0; 403 | 404 | } 405 | return true; 406 | } 407 | 408 | static private boolean SetWebViewerOutput(Context context) { 409 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 410 | 411 | boolean b = preferences.getBoolean("w1SWITCH", false); 412 | if (b) { 413 | String port = preferences.getString("w1PORT", ""); 414 | return AisCatcherJava.createWebViewer(port) == 0; 415 | 416 | } 417 | return true; 418 | } 419 | 420 | static private boolean SetSharing(Context context) { 421 | String defaultKey = "a6392e08-c57e-4e7a-a4fb-d73bfc7619ae"; 422 | 423 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 424 | 425 | boolean b = preferences.getBoolean("sSHARING", false); 426 | if (b) { 427 | String key = preferences.getString("sSHARINGKEY", defaultKey); 428 | if (key.equals("")) key = defaultKey; 429 | return AisCatcherJava.createSharing(b, key) == 0; 430 | 431 | } 432 | else 433 | AisCatcherJava.createSharing(b, defaultKey); 434 | return true; 435 | } 436 | 437 | } 438 | -------------------------------------------------------------------------------- /app/src/main/java/com/jvdegithub/aiscatcher/tools/InputFilterIP.java: -------------------------------------------------------------------------------- 1 | /* 2 | * AIS-catcher for Android 3 | * Copyright (C) 2022 jvde.github@gmail.com. 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 com.jvdegithub.aiscatcher.tools; 20 | 21 | import android.text.InputFilter; 22 | import android.text.Spanned; 23 | 24 | // See: https://stackoverflow.com/questions/31529651/edittext-android-filter-for-ip-address 25 | public class InputFilterIP implements InputFilter { 26 | 27 | @Override 28 | public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { 29 | 30 | if (end > start) { 31 | String r = dest.toString().substring(0, dstart) + source.subSequence(start, end) + dest.toString().substring(dend); 32 | if (!r.matches("^\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3})?)?)?)?)?)?")) { 33 | return ""; 34 | } else { 35 | String[] splits = r.split("\\."); 36 | for (String split : splits) { 37 | if (Integer.valueOf(split) > 255) return ""; 38 | } 39 | } 40 | } 41 | return null; 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jvdegithub/aiscatcher/tools/InputFilterMinMax.java: -------------------------------------------------------------------------------- 1 | /* 2 | * AIS-catcher for Android 3 | * Copyright (C) 2022 jvde.github@gmail.com. 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 com.jvdegithub.aiscatcher.tools; 20 | 21 | import android.text.InputFilter; 22 | import android.text.Spanned; 23 | 24 | public class InputFilterMinMax implements InputFilter { 25 | 26 | protected int min; 27 | protected int max; 28 | 29 | public InputFilterMinMax(int m, int M) 30 | { 31 | min = m; 32 | max = M; 33 | } 34 | 35 | @Override 36 | public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { 37 | 38 | String r = dest.toString().substring(0, dstart) + source.subSequence(start, end) + dest.toString().substring(dend); 39 | 40 | if (!r.matches("^([-]?|[-]?[1-9]\\d*|[0]|[1-9]?[1-9]\\d*)")) return ""; 41 | if(r.isEmpty()||r.equals("-")) return null; 42 | if ((Integer.parseInt(r) > max || Integer.parseInt(r) < min)) return ""; 43 | 44 | return null; 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jvdegithub/aiscatcher/tools/LogBook.java: -------------------------------------------------------------------------------- 1 | package com.jvdegithub.aiscatcher.tools; 2 | 3 | import java.text.MessageFormat; 4 | import java.util.ArrayList; 5 | import java.util.Date; 6 | import java.util.List; 7 | 8 | public class LogBook { 9 | 10 | private static LogBook instance; 11 | private List logs; 12 | private static final int MAX_LOGS = 50; 13 | 14 | private LogBook() { 15 | logs = new ArrayList<>(); 16 | } 17 | 18 | public static LogBook getInstance() { 19 | if (instance == null) { 20 | instance = new LogBook(); 21 | } 22 | return instance; 23 | } 24 | 25 | public void addLog(String log) { 26 | if (logs.size() >= MAX_LOGS) { 27 | logs.remove(0); 28 | } 29 | logs.add(MessageFormat.format("[{0, time}] {1}",new Date(), log)); 30 | notifyLogUpdate(log); 31 | } 32 | 33 | public List getLogs() { 34 | return logs; 35 | } 36 | 37 | public String getLogAsString() { 38 | StringBuilder stringBuilder = new StringBuilder(); 39 | for (String log : logs) { 40 | stringBuilder.append(log).append("
"); } 41 | return stringBuilder.toString(); 42 | } 43 | 44 | public interface LogUpdateListener { 45 | void onLogUpdated(String log); 46 | } 47 | 48 | private LogUpdateListener logUpdateListener; 49 | 50 | public void setLogUpdateListener(LogUpdateListener listener) { 51 | logUpdateListener = listener; 52 | } 53 | 54 | private void notifyLogUpdate(String log) { 55 | if (logUpdateListener != null) { 56 | logUpdateListener.onLogUpdated(log); 57 | } 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jvdegithub/aiscatcher/ui/main/StatisticsFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * AIS-catcher for Android 3 | * Copyright (C) 2022-2023 jvde.github@gmail.com. 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 com.jvdegithub.aiscatcher.ui.main; 20 | 21 | import android.os.Bundle; 22 | import android.view.LayoutInflater; 23 | import android.view.View; 24 | import android.view.ViewGroup; 25 | import android.widget.TextView; 26 | 27 | import androidx.annotation.NonNull; 28 | import androidx.annotation.Nullable; 29 | import androidx.fragment.app.Fragment; 30 | 31 | import com.jvdegithub.aiscatcher.AisCatcherJava; 32 | import com.jvdegithub.aiscatcher.MainActivity; 33 | import com.jvdegithub.aiscatcher.R; 34 | 35 | public class StatisticsFragment extends Fragment { 36 | 37 | TextView MB, Total, ChannelA, ChannelB, Msg123, Msg5, Msg1819, Msg24, MsgOther, WebPort; 38 | 39 | public static StatisticsFragment newInstance() { 40 | return new StatisticsFragment(); 41 | } 42 | 43 | @Override 44 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 45 | @Nullable Bundle savedInstanceState) { 46 | 47 | View rootview = inflater.inflate(R.layout.fragment_statistics, container, false); 48 | 49 | WebPort = rootview.findViewById(R.id.ServerPort); 50 | MB = rootview.findViewById(R.id.MBs); 51 | Total = rootview.findViewById(R.id.totalMSG); 52 | ChannelA = rootview.findViewById(R.id.ChannelAMSG); 53 | ChannelB = rootview.findViewById(R.id.ChannelBMSG); 54 | Msg123 = rootview.findViewById(R.id.MSG123); 55 | Msg5 = rootview.findViewById(R.id.MSG5); 56 | Msg1819 = rootview.findViewById(R.id.MSG1819); 57 | Msg24 = rootview.findViewById(R.id.MSG24); 58 | MsgOther = rootview.findViewById(R.id.MSGOther); 59 | 60 | Update(); 61 | return rootview; 62 | } 63 | 64 | @Override 65 | public void onCreate(Bundle savedInstanceState) { 66 | super.onCreate(savedInstanceState); 67 | } 68 | 69 | public void Update() { 70 | getActivity().runOnUiThread(() -> { 71 | WebPort.setText(String.format("%d", MainActivity.port)); 72 | MB.setText(AisCatcherJava.Statistics.getDataString()); 73 | Total.setText(String.format("%d", AisCatcherJava.Statistics.getTotal())); 74 | ChannelA.setText(String.format("%d", AisCatcherJava.Statistics.getChA())); 75 | ChannelB.setText(String.format("%d", AisCatcherJava.Statistics.getChB())); 76 | Msg123.setText(String.format("%d", AisCatcherJava.Statistics.getMsg123())); 77 | Msg5.setText(String.format("%d", AisCatcherJava.Statistics.getMsg5())); 78 | Msg1819.setText(String.format("%d", AisCatcherJava.Statistics.getMsg1819())); 79 | Msg24.setText(String.format("%d", AisCatcherJava.Statistics.getMsg24())); 80 | MsgOther.setText(String.format("%d", AisCatcherJava.Statistics.getMsgOther())); 81 | }); 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jvdegithub/aiscatcher/ui/main/WebViewMapFragment.java: -------------------------------------------------------------------------------- 1 | package com.jvdegithub.aiscatcher.ui.main; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.content.res.Configuration; 6 | import android.graphics.Bitmap; 7 | import android.net.ConnectivityManager; 8 | import android.net.NetworkCapabilities; 9 | import android.os.Bundle; 10 | 11 | import androidx.appcompat.app.AppCompatDelegate; 12 | import androidx.fragment.app.Fragment; 13 | import androidx.preference.PreferenceManager; 14 | 15 | import android.view.LayoutInflater; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.webkit.ConsoleMessage; 19 | import android.webkit.WebChromeClient; 20 | import android.webkit.WebResourceRequest; 21 | import android.webkit.WebResourceResponse; 22 | import android.webkit.WebSettings; 23 | import android.webkit.WebView; 24 | import android.webkit.WebViewClient; 25 | 26 | import com.jvdegithub.aiscatcher.MainActivity; 27 | import com.jvdegithub.aiscatcher.R; 28 | import com.jvdegithub.aiscatcher.tools.LogBook; 29 | 30 | import java.io.IOException; 31 | import java.io.InputStream; 32 | 33 | public class WebViewMapFragment extends Fragment { 34 | 35 | private WebView webView; 36 | private LogBook logbook; 37 | private SharedPreferences sharedPreferences; 38 | private Context context; 39 | 40 | public static WebViewMapFragment newInstance() { 41 | return new WebViewMapFragment(); 42 | } 43 | 44 | private boolean isOnline() { 45 | ConnectivityManager connectivityManager = (ConnectivityManager) getActivity().getSystemService(Context.CONNECTIVITY_SERVICE); 46 | NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork()); 47 | return networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); 48 | } 49 | 50 | private SharedPreferences getSharedPreferences() { 51 | if (sharedPreferences == null && context != null) { 52 | sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); 53 | } 54 | return sharedPreferences; 55 | } 56 | 57 | @Override 58 | public void onAttach(Context context) { 59 | super.onAttach(context); 60 | this.context = context; 61 | } 62 | 63 | @Override 64 | public void onDetach() { 65 | super.onDetach(); 66 | context = null; 67 | } 68 | 69 | @Override 70 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 71 | logbook = LogBook.getInstance(); 72 | 73 | View rootView = inflater.inflate(R.layout.fragment_map, container, false); 74 | webView = rootView.findViewById(R.id.webmap); 75 | WebSettings webSettings = webView.getSettings(); 76 | webSettings.setJavaScriptEnabled(true); 77 | webSettings.setDomStorageEnabled(true); 78 | 79 | webView.setWebViewClient(new WebViewClient() { 80 | @Override 81 | public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 82 | String url = request.getUrl().toString(); 83 | 84 | if (url.startsWith("https://cdn.jsdelivr.net/") || url.startsWith("https://unpkg.com/")) { 85 | String prefix = url.startsWith("https://cdn.jsdelivr.net/") ? "https://cdn.jsdelivr.net/" : "https://unpkg.com/"; 86 | 87 | String remainingPath = "webassets/cdn/" + url.substring(prefix.length()); 88 | 89 | try { 90 | if (context == null) { 91 | return null; 92 | } 93 | 94 | InputStream inputStream = context.getAssets().open(remainingPath); 95 | 96 | String contentType; 97 | if (remainingPath.endsWith(".css")) { 98 | contentType = "text/css"; 99 | } else if (remainingPath.endsWith(".svg")) { 100 | contentType = "image/svg+xml"; 101 | } else if (remainingPath.endsWith(".png")) { 102 | contentType = "image/png"; 103 | } else if (remainingPath.endsWith(".js")) { 104 | contentType = "text/plain"; 105 | } else return null; 106 | 107 | WebResourceResponse response = new WebResourceResponse(contentType, "UTF-8", inputStream); 108 | 109 | return response; 110 | } catch (IOException e) { 111 | logbook.addLog("Cannot load " + remainingPath); 112 | } 113 | } 114 | 115 | return null; 116 | } 117 | 118 | @Override 119 | public void onPageStarted(WebView view, String url, Bitmap favicon) { 120 | webView.setVisibility(View.INVISIBLE); 121 | 122 | // Restore localStorage content as early as possible 123 | if(getSharedPreferences()!= null) { 124 | String localStorageContent = getSharedPreferences().getString("localStorageContent", null); 125 | if (localStorageContent != null) { 126 | webView.evaluateJavascript("localStorage.setItem('settings', " + localStorageContent + ");", null); 127 | } 128 | } 129 | } 130 | 131 | @Override 132 | public void onPageFinished(WebView view, String url) { 133 | webView.setVisibility(View.VISIBLE); 134 | } 135 | }); 136 | 137 | webView.setWebChromeClient(new WebChromeClient() { 138 | @Override 139 | public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 140 | logbook.addLog(String.format("W(%d): %s", 141 | consoleMessage.lineNumber(), consoleMessage.message())); 142 | 143 | return true; 144 | } 145 | }); 146 | 147 | String url = "http://localhost:" + MainActivity.port + "?welcome=false&android=true"; 148 | 149 | int currentNightMode = AppCompatDelegate.getDefaultNightMode(); 150 | if ((getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES) 151 | url += "&dark_mode=true"; 152 | else 153 | url += "&dark_mode=false"; 154 | webView.loadUrl(url); 155 | logbook.addLog("Opening: " + url); 156 | 157 | return rootView; 158 | } 159 | 160 | @Override 161 | public void onPause() { 162 | super.onPause(); 163 | webView.evaluateJavascript("localStorage.getItem('settings');", value -> { 164 | if (value != null && !value.equals("null")) { 165 | getSharedPreferences().edit().putString("localStorageContent", value).apply(); 166 | } 167 | }); 168 | } 169 | 170 | @Override 171 | public void onDestroyView() { 172 | super.onDestroyView(); 173 | webView.stopLoading(); 174 | logbook.addLog("View is destroyed."); 175 | } 176 | } -------------------------------------------------------------------------------- /app/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | # Sets the minimum version of CMake required to build the native library. 3 | 4 | cmake_minimum_required(VERSION 3.10.2) 5 | 6 | # Declares and names the project. 7 | 8 | project("AIScatcherNDK") 9 | 10 | #set(OPTIMIZATION_FLAGS "-Ofast") 11 | set(OPTIMIZATION_FLAGS "-g") 12 | 13 | set(CMAKE_CXX_FLAGS "${OPTIMIZATION_FLAGS}") 14 | set(CMAKE_C_FLAGS "${OPTIMIZATION_FLAGS}") 15 | 16 | # Creates and names a library, sets it as either STATIC 17 | # or SHARED, and provides the relative paths to its source code. 18 | # You can define multiple libraries, and CMake builds them for you. 19 | # Gradle automatically packages shared libraries with your APK. 20 | 21 | add_library( # Sets the name of the library. 22 | AIScatcherNDK 23 | 24 | # Sets the library as a shared library. 25 | SHARED 26 | 27 | # Provides a relative path to your source file(s). 28 | ./libusb/libusb/core.c 29 | ./libusb/libusb/descriptor.c 30 | ./libusb/libusb/hotplug.c 31 | ./libusb/libusb/io.c 32 | ./libusb/libusb/sync.c 33 | ./libusb/libusb/strerror.c 34 | ./libusb/libusb/os/linux_usbfs.c 35 | ./libusb/libusb/os/events_posix.c 36 | ./libusb/libusb/os/threads_posix.c 37 | ./libusb/libusb/os/linux_netlink.c 38 | 39 | ./rtl-sdr/src/librtlsdr.c 40 | ./rtl-sdr/src/tuner_e4k.c ./rtl-sdr/src/tuner_fc0012.c ./rtl-sdr/src/tuner_fc0013.c ./rtl-sdr/src/tuner_fc2580.c ./rtl-sdr/src/tuner_r82xx.c 41 | 42 | ./airspyone_host/libairspy/src/airspy.c ./airspyone_host/libairspy/src/iqconverter_float.c 43 | ./airspyone_host/libairspy/src/iqconverter_int16.c 44 | 45 | ./airspyhf/libairspyhf/src/airspyhf.c 46 | ./airspyhf/libairspyhf/src/iqbalancer.c 47 | 48 | ./AIS-catcher/Application/Receiver.cpp ./AIS-catcher/Application/WebDB.cpp ./AIS-catcher/Application/Config.cpp ./AIS-catcher/Tracking/DB.cpp ./AIS-catcher/Tracking/Ships.cpp ./AIS-catcher/DBMS/PostgreSQL.cpp 49 | ./AIS-catcher/Device/AIRSPYHF.cpp ./AIS-catcher/Device/FileWAV.cpp ./AIS-catcher/Device/RTLSDR.cpp ./AIS-catcher/Device/SDRPLAY.cpp ./AIS-catcher/DSP/Demod.cpp ./AIS-catcher/DSP/Model.cpp 50 | ./AIS-catcher/Library/AIS.cpp ./AIS-catcher/Library/JSONAIS.cpp ./AIS-catcher/Library/Keys.cpp ./AIS-catcher/IO/HTTPClient.cpp 51 | ./AIS-catcher/Device/FileRAW.cpp ./AIS-catcher/Device/HACKRF.cpp ./AIS-catcher/Device/UDP.cpp ./AIS-catcher/Device/RTLTCP.cpp 52 | ./AIS-catcher/Device/ZMQ.cpp ./AIS-catcher/Device/SoapySDR.cpp ./AIS-catcher/Device/SpyServer.cpp ./AIS-catcher/Library/Message.cpp ./AIS-catcher/Library/NMEA.cpp 53 | ./AIS-catcher/Library/Utilities.cpp ./AIS-catcher/Library/TCP.cpp ./AIS-catcher/JSON/JSON.cpp ./AIS-catcher/IO/Network.cpp ./AIS-catcher/IO/HTTPServer.cpp 54 | ./AIS-catcher/JSON/StringBuilder.cpp ./AIS-catcher/JSON/Parser.cpp ./AIS-catcher/Device/AIRSPY.cpp ./AIS-catcher/Device/Serial.cpp ./AIS-catcher/Library/ADSB.cpp 55 | ./AIS-catcher/DSP/DSP.cpp ./AIS-catcher/Application/WebViewer.cpp ./AIS-catcher/Application/Prometheus.cpp ./AIS-catcher/Protocol/Protocol.cpp ./AIS-catcher/Library/Logger.cpp 56 | 57 | JNI/AIScatcherNDK.cpp) 58 | 59 | include_directories( 60 | ./libusb/android ./libusb/libusb ./libusb/libusb/os 61 | ./rtl-sdr/include 62 | ./airspyone_host/libairspy/src 63 | ./airspyhf/libairspyhf/src 64 | ./AIS-catcher ./AIS-catcher/Application ./AIS-catcher/IO ./AIS-catcher/Library ./AIS-catcher/Tracking ./AIS-catcher/DBMS ./AIS-catcher/DSP ./AIS-catcher/Device ./AIS-catcher/Protocol) 65 | 66 | add_definitions(-DHASRTLSDR -DHASRTLSDR_BIASTEE -DHASRTL_ANDROID -DHASAIRSPY -DHASAIRSPY_ANDROID -D HASAIRSPYHF -DHASAIRSPYHF_ANDROID -DHASRTLSDR_TUNERBW) 67 | 68 | # Searches for a specified prebuilt library and stores the path as a 69 | # variable. Because CMake includes system libraries in the search path by 70 | # default, you only need to specify the name of the public NDK library 71 | # you want to add. CMake verifies that the library exists before 72 | # completing its build. 73 | 74 | find_library( # Sets the name of the path variable. 75 | log-lib 76 | 77 | # Specifies the name of the NDK library that 78 | # you want CMake to locate. 79 | log) 80 | 81 | # Specifies libraries CMake should link to your target library. You 82 | # can link multiple libraries, such as libraries you define in this 83 | # build script, prebuilt third-party libraries, or system libraries. 84 | 85 | target_link_libraries( # Specifies the target library. 86 | AIScatcherNDK 87 | 88 | # Links the target library to the log library 89 | # included in the NDK. 90 | ${log-lib}) 91 | -------------------------------------------------------------------------------- /app/src/main/jni/JNI/AIScatcherNDK.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * AIS-catcher for Android 3 | * Copyright (C) 2022 jvde.github@gmail.com. 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 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | 26 | const int TIME_CONSTRAINT = 120; 27 | bool communityFeed = false; 28 | 29 | #define LOG_TAG "AIS-catcher JNI" 30 | 31 | #define LOGE(...) \ 32 | __android_log_print(ANDROID_LO \ 33 | G_ERROR, LOG_TAG, __VA_ARGS__) 34 | 35 | #include "AIS-catcher.h" 36 | #include "Application/version.h" 37 | #include "Receiver.h" 38 | #include "JSONAIS.h" 39 | #include "Signals.h" 40 | #include "Common.h" 41 | #include "Model.h" 42 | #include "Network.h" 43 | #include "WebViewer.h" 44 | #include "Logger.h" 45 | 46 | #include "Device/RTLSDR.h" 47 | #include "Device/AIRSPYHF.h" 48 | #include "Device/HACKRF.h" 49 | #include "Device/RTLTCP.h" 50 | #include "Device/SpyServer.h" 51 | #include "Device/AIRSPY.h" 52 | 53 | static int javaVersion; 54 | 55 | static JavaVM *javaVm = nullptr; 56 | static jclass javaClass = nullptr; 57 | static jclass javaStatisticsClass = nullptr; 58 | 59 | struct Statistics { 60 | uint64_t DataSize; 61 | int Total; 62 | int ChA; 63 | int ChB; 64 | int Msg[28]; 65 | int Error; 66 | 67 | } statistics; 68 | 69 | WebViewer server; 70 | static std::unique_ptr webviewer = nullptr; 71 | static std::unique_ptr TCP_listener = nullptr; 72 | int webviewer_port = -1; 73 | 74 | std::string nmea_msg; 75 | std::string json_queue; 76 | 77 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *, void *) { 78 | return JNI_VERSION_1_6; 79 | } 80 | 81 | /* 82 | void Attach(JNIEnv *env) 83 | { 84 | if (javaVm->GetEnv((void **) &env, javaVersion) == JNI_EDETACHED) 85 | javaVm->AttachCurrentThread(&env, nullptr); 86 | } 87 | 88 | void DetachThread() 89 | { 90 | javaVm->DetachCurrentThread(); 91 | } 92 | */ 93 | 94 | // JAVA interaction and callbacks 95 | 96 | std::string toString(JNIEnv* env, jstring jStr) { 97 | 98 | if (!jStr) { 99 | return std::string(); 100 | } 101 | 102 | const char* chars = env->GetStringUTFChars(jStr, nullptr); 103 | if (!chars) return std::string(); 104 | 105 | std::string result(chars); 106 | env->ReleaseStringUTFChars(jStr, chars); 107 | return result; 108 | } 109 | 110 | void pushStatistics(JNIEnv *env) { 111 | 112 | const int GB = 1000000000; 113 | env->SetStaticIntField(javaStatisticsClass, 114 | env->GetStaticFieldID(javaStatisticsClass, "DataB", "I"), 115 | (int)(statistics.DataSize % GB)); 116 | env->SetStaticIntField(javaStatisticsClass, 117 | env->GetStaticFieldID(javaStatisticsClass, "DataGB", "I"), 118 | (int)(statistics.DataSize / GB)); 119 | env->SetStaticIntField(javaStatisticsClass, 120 | env->GetStaticFieldID(javaStatisticsClass, "Total", "I"), 121 | statistics.Total); 122 | env->SetStaticIntField(javaStatisticsClass, 123 | env->GetStaticFieldID(javaStatisticsClass, "ChA", "I"), statistics.ChA); 124 | env->SetStaticIntField(javaStatisticsClass, 125 | env->GetStaticFieldID(javaStatisticsClass, "ChB", "I"), statistics.ChB); 126 | env->SetStaticIntField(javaStatisticsClass, 127 | env->GetStaticFieldID(javaStatisticsClass, "Msg123", "I"), 128 | statistics.Msg[1] + statistics.Msg[2] + statistics.Msg[3]); 129 | env->SetStaticIntField(javaStatisticsClass, 130 | env->GetStaticFieldID(javaStatisticsClass, "Msg5", "I"), 131 | statistics.Msg[5]); 132 | env->SetStaticIntField(javaStatisticsClass, 133 | env->GetStaticFieldID(javaStatisticsClass, "Msg1819", "I"), 134 | statistics.Msg[18] + statistics.Msg[19]); 135 | env->SetStaticIntField(javaStatisticsClass, 136 | env->GetStaticFieldID(javaStatisticsClass, "Msg24", "I"), 137 | statistics.Msg[24] + statistics.Msg[25]); 138 | env->SetStaticIntField(javaStatisticsClass, 139 | env->GetStaticFieldID(javaStatisticsClass, "MsgOther", "I"), 140 | statistics.Total - 141 | (statistics.Msg[1] + statistics.Msg[2] + statistics.Msg[3] + 142 | statistics.Msg[5] + statistics.Msg[18] + statistics.Msg[19] + 143 | statistics.Msg[24] + statistics.Msg[25])); 144 | } 145 | 146 | static void callbackNMEA(JNIEnv *env, const std::string &str) { 147 | 148 | jstring jstr = env->NewStringUTF(str.c_str()); 149 | jmethodID method = env->GetStaticMethodID(javaClass, "onNMEA", "(Ljava/lang/String;)V"); 150 | env->CallStaticVoidMethod(javaClass, method, jstr); 151 | } 152 | 153 | static void callbackMessage(JNIEnv *env, const std::string &str) { 154 | 155 | jstring jstr = env->NewStringUTF(str.c_str()); 156 | jmethodID method = env->GetStaticMethodID(javaClass, "onMessage", "(Ljava/lang/String;)V"); 157 | env->CallStaticVoidMethod(javaClass, method, jstr); 158 | } 159 | 160 | static void callbackConsole(JNIEnv *env, const std::string &str) { 161 | 162 | jstring jstr = env->NewStringUTF(str.c_str()); 163 | jmethodID method = env->GetStaticMethodID(javaClass, "onStatus", "(Ljava/lang/String;)V"); 164 | env->CallStaticVoidMethod(javaClass, method, jstr); 165 | } 166 | 167 | static void callbackConsoleFormat(JNIEnv *env, const char *format, ...) { 168 | 169 | char buffer[256]; 170 | va_list args; 171 | va_start (args, format); 172 | vsnprintf(buffer, 255, format, args); 173 | 174 | jstring jstr = env->NewStringUTF(buffer); 175 | jmethodID method = env->GetStaticMethodID(javaClass, "onStatus", "(Ljava/lang/String;)V"); 176 | env->CallStaticVoidMethod(javaClass, method, jstr); 177 | 178 | va_end (args); 179 | } 180 | 181 | static void callbackUpdate(JNIEnv *env) { 182 | 183 | pushStatistics(env); 184 | 185 | jmethodID method = env->GetStaticMethodID(javaClass, "onUpdate", "()V"); 186 | env->CallStaticVoidMethod(javaClass, method); 187 | } 188 | 189 | static void callbackError(JNIEnv *env, const std::string &str) { 190 | 191 | callbackConsole(env, str+"\r\n"); 192 | Error() << str; 193 | 194 | jstring jstr = env->NewStringUTF(str.c_str()); 195 | jmethodID method = env->GetStaticMethodID(javaClass, "onError", "(Ljava/lang/String;)V"); 196 | env->CallStaticVoidMethod(javaClass, method, jstr); 197 | } 198 | 199 | // AIS-catcher model 200 | 201 | class NMEAcounter : public StreamIn { 202 | std::string list; 203 | bool clean = true; 204 | 205 | public: 206 | 207 | void Receive(const AIS::Message *data, int len, TAG &tag) { 208 | std::string str; 209 | 210 | for (int i = 0; i < len; i++) { 211 | for (const auto &s: data[i].NMEA) { 212 | str.append("\n" + s); 213 | } 214 | 215 | statistics.Total++; 216 | 217 | if (data[i].getChannel() == 'A') 218 | statistics.ChA++; 219 | else 220 | statistics.ChB++; 221 | 222 | int msg = data[i].type(); 223 | 224 | if (msg > 27 || msg < 1) statistics.Error++; 225 | if (msg <= 27) statistics.Msg[msg]++; 226 | 227 | nmea_msg += str; 228 | } 229 | } 230 | }; 231 | 232 | // Counting Data received from device 233 | class RAWcounter : public StreamIn { 234 | public: 235 | 236 | void Receive(const RAW *data, int len, TAG &tag) { 237 | statistics.DataSize += data->size; 238 | } 239 | }; 240 | 241 | struct Drivers { 242 | Device::RTLSDR RTLSDR; 243 | Device::RTLTCP RTLTCP; 244 | Device::SpyServer SPYSERVER; 245 | Device::AIRSPY AIRSPY; 246 | Device::AIRSPYHF AIRSPYHF; 247 | } drivers; 248 | 249 | std::vector UDP_connections; 250 | std::vector TCP_connections; 251 | std::vector UDPhost; 252 | std::vector UDPport; 253 | std::vector UDPJSON; 254 | std::string TCP_listener_port; 255 | 256 | bool sharing = false; 257 | std::string sharingKey = ""; 258 | 259 | NMEAcounter NMEAcounter; 260 | RAWcounter rawcounter; 261 | 262 | Device::Device *device = nullptr; 263 | static std::unique_ptr model = nullptr; 264 | AIS::JSONAIS json2ais; 265 | 266 | bool stop = false; 267 | 268 | void StopRequest() { 269 | stop = true; 270 | } 271 | 272 | extern "C" 273 | JNIEXPORT jint JNICALL 274 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_InitNative(JNIEnv *env, jclass instance, jint port) { 275 | Logger::getInstance().setMaxBufferSize(50); 276 | 277 | env->GetJavaVM(&javaVm); 278 | javaVersion = env->GetVersion(); 279 | javaClass = (jclass) env->NewGlobalRef(instance); 280 | 281 | Info() << "AIS-Catcher " VERSION; 282 | Info() << "Internal webserver running at port " << port; 283 | 284 | memset(&statistics, 0, sizeof(statistics)); 285 | 286 | server.Set("PORT",std::to_string(port)); 287 | server.Set("STATION","Android"); 288 | server.Set("SHARE_LOC","ON"); 289 | server.Set("REALTIME","ON"); 290 | server.Set("LOG","ON"); 291 | server.start(); 292 | 293 | return 0; 294 | } 295 | 296 | extern "C" 297 | JNIEXPORT jboolean JNICALL 298 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_isStreaming(JNIEnv *, jclass) { 299 | if (device && device->isStreaming()) return JNI_TRUE; 300 | 301 | return JNI_FALSE; 302 | } 303 | 304 | extern "C" 305 | JNIEXPORT jint JNICALL 306 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_applySetting(JNIEnv *env, jclass, jstring dev, jstring setting, jstring param) { 307 | 308 | try { 309 | std::string d = toString(env,dev); 310 | std::string s = toString(env, setting); 311 | std::string p = toString(env, param); 312 | 313 | switch (d[0]) { 314 | case 't': 315 | Info() << "RTLTCP: " << s << " = " << p; 316 | drivers.RTLTCP.Set(s, p); 317 | break; 318 | case 'r': 319 | Info() << "RTLSDR: " << s << " = " << p; 320 | drivers.RTLSDR.Set(s, p); 321 | break; 322 | case 'm': 323 | Info() << "AIRSPY: " << s << " = " << p; 324 | drivers.AIRSPY.Set(s, p); 325 | break; 326 | case 'h': 327 | Info() << "AIRSPYHF: " << s << " = " << p; 328 | drivers.AIRSPYHF.Set(s, p); 329 | break; 330 | case 's': 331 | Info() << "SPYSERVER: " << s << " = " << p; 332 | drivers.SPYSERVER.Set(s, p); 333 | break; 334 | 335 | } 336 | 337 | } catch (std::exception& e) { 338 | callbackError(env, e.what()); 339 | device = nullptr; 340 | return -1; 341 | } 342 | return 0; 343 | } 344 | 345 | extern "C" 346 | JNIEXPORT jint JNICALL 347 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_Run(JNIEnv *env, jclass) { 348 | 349 | 350 | const int TIME_INTERVAL = 1000; 351 | const int TIME_MAX = (TIME_CONSTRAINT * 1000) / TIME_INTERVAL; 352 | TAG tag; 353 | tag.mode = 7; 354 | 355 | try { 356 | Info() << "Creating UDP output channels"; 357 | UDP_connections.resize(UDPhost.size()); 358 | 359 | for (int i = 0; i < UDPhost.size(); i++) { 360 | UDP_connections[i].Set("host",UDPhost[i]).Set("port",UDPport[i]).Set("JSON",UDPJSON[i]?"on":"off"); 361 | UDP_connections[i].Start(); 362 | model->Output() >> UDP_connections[i]; 363 | } 364 | 365 | if(!TCP_listener_port.empty()) { 366 | TCP_listener = std::make_unique(); 367 | Info() << "Creating TCP listener at port " << TCP_listener_port; 368 | TCP_listener->Set("PORT", TCP_listener_port); 369 | TCP_listener->Set("TIMEOUT","0"); 370 | TCP_listener->Set("JSON","false"); 371 | model->Output() >> (*TCP_listener); 372 | } 373 | 374 | if(sharing) { 375 | Info() << "Creating Sharing output channel"; 376 | int sharing_index = TCP_connections.size(); 377 | TCP_connections.resize(sharing_index + 1); 378 | 379 | TCP_connections[sharing_index].Set("HOST", "aiscatcher.org").Set("PORT", "4242").Set("JSON", "on").Set("FILTER", "on").Set("GPS", "off"); 380 | TCP_connections[sharing_index].Set("UUID", sharingKey); 381 | TCP_connections[sharing_index].Start(); 382 | model->Output() >> TCP_connections[sharing_index]; 383 | } 384 | 385 | if(webviewer) { 386 | Info() << "Starting Web Viewer"; 387 | webviewer->start(); 388 | } 389 | 390 | if(TCP_listener) { 391 | Info() << "Starting TCP listener"; 392 | TCP_listener->Start(); 393 | } 394 | Info() << "Start Device"; 395 | device->setTag(tag); 396 | device->Play(); 397 | 398 | stop = false; 399 | 400 | Info() << "Run Started"; 401 | 402 | int time_idx = 0; 403 | 404 | while (device->isStreaming() && !stop) { 405 | std::this_thread::sleep_for(std::chrono::milliseconds(TIME_INTERVAL)); 406 | 407 | callbackUpdate(env); 408 | if (!nmea_msg.empty()) { 409 | callbackNMEA(env, nmea_msg); 410 | nmea_msg = ""; 411 | } 412 | if(++time_idx % 30 == 0) 413 | Info() << "Msg Count: " << statistics.Total; 414 | } 415 | 416 | } 417 | catch (std::exception& e) { 418 | callbackError(env, e.what()); 419 | } 420 | 421 | try { 422 | device->Stop(); 423 | 424 | model->Output().out.clear(); 425 | 426 | for (auto &u: UDP_connections) u.Stop(); 427 | for (auto &t: TCP_connections) t.Stop(); 428 | 429 | if(TCP_listener) TCP_listener->Stop(); 430 | 431 | UDP_connections.clear(); 432 | TCP_connections.clear(); 433 | 434 | UDPport.clear(); 435 | UDPhost.clear(); 436 | UDPJSON.clear(); 437 | TCP_listener = nullptr; 438 | 439 | if(webviewer) { 440 | webviewer->close(); 441 | webviewer.reset(); 442 | } 443 | webviewer_port = -1; 444 | 445 | } catch (std::exception& e) { 446 | callbackError(env, e.what()); 447 | } 448 | 449 | if (!stop) { 450 | callbackError(env, "Device disconnected"); 451 | } 452 | 453 | return 0; 454 | } 455 | 456 | extern "C" 457 | JNIEXPORT jint JNICALL 458 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_Close(JNIEnv *env, jclass) { 459 | Info() << "Device closing"; 460 | 461 | try { 462 | if (device) device->Close(); 463 | device = nullptr; 464 | model.reset(); 465 | } 466 | catch (std::exception& e) { 467 | callbackError(env, e.what()); 468 | return -1; 469 | } 470 | return 0; 471 | } 472 | 473 | extern "C" 474 | JNIEXPORT jint JNICALL 475 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_forceStop(JNIEnv *env, jclass) { 476 | Info() << "Stop requested"; 477 | stop = true; 478 | return 0; 479 | } 480 | 481 | extern "C" 482 | JNIEXPORT jint JNICALL 483 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_createReceiver(JNIEnv *env, jclass, jint source, 484 | jint fd, jint CGF_wide, jint model_type, jint FPDS) { 485 | 486 | Info() << "Creating Receiver (source = " << static_cast(source) 487 | << ", fd = " << static_cast(fd) 488 | << ", CGF wide = " << static_cast(CGF_wide) 489 | << ", model = " << static_cast(model_type) 490 | << ", FPDS = " << static_cast(FPDS) << ")" << std::endl; 491 | /* 492 | if (device != nullptr) { 493 | callbackConsole(env, "Error: device already assigned."); 494 | return -1; 495 | } 496 | */ 497 | 498 | if (source == 0) { 499 | Info() << "Device: RTLTCP"; 500 | device = &drivers.RTLTCP; 501 | } else if (source == 1) { 502 | Info() << "Device: RTLSDR"; 503 | device = &drivers.RTLSDR; 504 | } else if (source == 2) { 505 | Info() << "Device: AIRSPY"; 506 | device = &drivers.AIRSPY; 507 | } else if (source == 3) { 508 | Info() << "Device: AIRSPYHF"; 509 | device = &drivers.AIRSPYHF; 510 | } else if (source == 4) { 511 | Info() << "Device: SPYSERVER"; 512 | device = &drivers.SPYSERVER; 513 | } else { 514 | Info() << "Support for this device not included."; 515 | return -1; 516 | } 517 | 518 | try { 519 | device->out.clear(); 520 | device->OpenWithFileDescriptor(fd); 521 | device->setFrequency(162000000); 522 | } 523 | catch (std::exception& e) { 524 | callbackError(env, e.what()); 525 | device = nullptr; 526 | return -1; 527 | } 528 | 529 | Info() << "Creating Model"; 530 | try { 531 | 532 | Info() << "Building model with sampling rate: " << device->getSampleRate() / 1000; 533 | 534 | model.reset(); 535 | 536 | if(device && device->getFormat() == Format::TXT) { 537 | Info() << "Model: NMEA"; 538 | 539 | model = std::make_unique(); 540 | model->buildModel('A','B',device->getSampleRate(), false, device); 541 | } 542 | else { 543 | if(model_type == 0) { 544 | 545 | Info() << "Model: default"; 546 | model = std::make_unique(); 547 | 548 | std::string s = (CGF_wide == 0)?"OFF":"ON"; 549 | model->Set("AFC_WIDE",s); 550 | Info() << "AFC Wide " << s; 551 | } 552 | else { 553 | Info() << "Model: Base (FM)"; 554 | model = std::make_unique(); 555 | } 556 | 557 | std::string s = (FPDS == 0)?"OFF":"ON"; 558 | model->Set("FP_DS",s); 559 | Info() << "Fixed Point Downsampler: " << s; 560 | 561 | model->buildModel('A','B',device->getSampleRate(), false, device); 562 | } 563 | 564 | } catch (std::exception& e) { 565 | callbackError(env, e.what()); 566 | device = nullptr; 567 | return -1; 568 | } 569 | 570 | device->out >> rawcounter; 571 | model->Output() >> NMEAcounter; 572 | json2ais.out.clear(); 573 | model->Output() >> json2ais; 574 | server.connect(*model, json2ais.out, *device); 575 | 576 | Info() << "Creating additional Web Viewer"; 577 | 578 | if(webviewer) webviewer.reset(); 579 | 580 | if(webviewer_port != -1) { 581 | webviewer = std::make_unique(); 582 | 583 | if(!webviewer) { 584 | Critical() << "Cannot create Web Viewer"; 585 | throw std::runtime_error("Cannot create Web Viewer)"); 586 | } 587 | 588 | webviewer->Set("PORT", std::to_string(webviewer_port)); 589 | webviewer->Set("STATION", "Android"); 590 | webviewer->Set("SHARE_LOC","ON"); 591 | webviewer->Set("REALTIME","ON"); 592 | } 593 | 594 | if(webviewer && webviewer_port != -1) 595 | webviewer->connect(*model, json2ais.out, *device); 596 | 597 | return 0; 598 | } 599 | 600 | extern "C" 601 | JNIEXPORT jint JNICALL 602 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_createUDP(JNIEnv *env, jclass clazz, jstring h, 603 | jstring p, jboolean J) { 604 | try { 605 | UDPport.resize(UDPport.size() + 1); 606 | UDPhost.resize(UDPhost.size() + 1); 607 | UDPJSON.resize(UDPJSON.size() + 1); 608 | 609 | jboolean b; 610 | std::string host = toString(env,h); //(env)->GetStringUTFChars(h, &b); 611 | std::string port = toString(env, p); //(env)->GetStringUTFChars(p, &b); 612 | bool JSON = J; 613 | 614 | UDPport[UDPport.size() - 1] = port; 615 | UDPhost[UDPhost.size() - 1] = host; 616 | UDPJSON[UDPJSON.size()-1] = JSON; 617 | 618 | Info() << "UDP: " << host << ":" << port << (J ? std::string(" (JSON)") : std::string(" (NMEA)")); 619 | } catch (std::exception& e) { 620 | callbackError(env, e.what()); 621 | device = nullptr; 622 | return -1; 623 | } 624 | return 0; 625 | } 626 | 627 | extern "C" 628 | JNIEXPORT jint JNICALL 629 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_createWebViewer(JNIEnv *env, jclass clazz, 630 | jstring p) { 631 | try { 632 | jboolean b; 633 | std::string port = toString(env,p); //(env)->GetStringUTFChars(p, &b); 634 | webviewer_port = std::stoi(port); 635 | 636 | Info() << "Web Viewer active on port " << port; 637 | 638 | } catch (std::exception& e) { 639 | callbackError(env, e.what()); 640 | device = nullptr; 641 | return -1; 642 | } 643 | return 0;} 644 | 645 | extern "C" 646 | JNIEXPORT jint JNICALL 647 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_createSharing(JNIEnv *env, jclass clazz, jboolean b, 648 | jstring k) { 649 | if(b) { 650 | jboolean isCopy; 651 | std::string key = toString(env,k); //(env)->GetStringUTFChars(k, &isCopy); 652 | 653 | sharing = communityFeed = true; 654 | sharingKey = key; 655 | Info() << "Community Sharing: " << key; 656 | } 657 | else { 658 | sharing = communityFeed = false; 659 | sharingKey = ""; 660 | } 661 | return 0; 662 | } 663 | 664 | extern "C" 665 | JNIEXPORT jint JNICALL 666 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_getSampleRate(JNIEnv *env, jclass clazz) { 667 | if (device == NULL) return 0; 668 | 669 | return device->getSampleRate(); 670 | } 671 | 672 | 673 | extern "C" 674 | JNIEXPORT void JNICALL 675 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_00024Statistics_Init(JNIEnv *env, jclass instance) { 676 | javaStatisticsClass = (jclass) env->NewGlobalRef(instance); 677 | memset(&statistics, 0, sizeof(statistics)); 678 | } 679 | 680 | extern "C" 681 | JNIEXPORT void JNICALL 682 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_00024Statistics_Reset(JNIEnv *env, jclass instance) { 683 | 684 | memset(&statistics, 0, sizeof(statistics)); 685 | 686 | server.Reset(); 687 | 688 | callbackUpdate(env); 689 | callbackNMEA(env, ""); 690 | } 691 | 692 | extern "C" 693 | JNIEXPORT void JNICALL 694 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_setLatLon(JNIEnv *env, jclass clazz, jfloat lat, 695 | jfloat lon) { 696 | server.Set("LAT",std::to_string(lat)); 697 | server.Set("LON",std::to_string(lon)); 698 | 699 | if(webviewer) { 700 | webviewer->Set("LAT",std::to_string(lat)); 701 | webviewer->Set("LON",std::to_string(lon)); 702 | } 703 | } 704 | 705 | extern "C" 706 | JNIEXPORT jstring JNICALL 707 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_getLibraryVersion(JNIEnv *env, jobject thiz) { 708 | std::string message = VERSION_DESCRIBE; 709 | return env->NewStringUTF(message.c_str()); 710 | } 711 | 712 | extern "C" 713 | JNIEXPORT void JNICALL 714 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_setDeviceDescription(JNIEnv *env, jclass clazz, 715 | jstring p, jstring v, 716 | jstring s) { 717 | 718 | std::string product = toString(env,p); 719 | std::string vendor = toString(env,v); 720 | std::string serial = toString(env, s); 721 | 722 | server.setDeviceDescription(product.c_str(), vendor.c_str(), serial.c_str()); 723 | } 724 | 725 | extern "C" 726 | JNIEXPORT jstring JNICALL 727 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_getRateDescription(JNIEnv *env, jclass clazz) { 728 | return env->NewStringUTF(device->getRateDescription().c_str()); 729 | } 730 | 731 | 732 | extern "C" 733 | JNIEXPORT jint JNICALL 734 | Java_com_jvdegithub_aiscatcher_AisCatcherJava_createTCPlistener(JNIEnv *env, jclass clazz, 735 | jstring p) { 736 | std::string port = toString(env, p); 737 | TCP_listener_port = port; 738 | Info() << "TCP Listener: " << port ; 739 | return 0; 740 | } -------------------------------------------------------------------------------- /app/src/main/res/color/bottom_navigation_colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v24/ic_notif_launcher.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_notif_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/drawable-hdpi/ic_notif_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_notif_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/drawable-mdpi/ic_notif_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_notif_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/drawable-xhdpi/ic_notif_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notif_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/drawable-xxhdpi/ic_notif_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_clear_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_content_copy_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_input_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_play_circle_filled_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_settings_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_stop_circle_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_web_asset_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 17 | 23 | 29 | 35 | 41 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo.xml: -------------------------------------------------------------------------------- 1 | 7 | 13 | 19 | 25 | 31 | 37 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 18 | 19 | 25 | 26 | 27 | 28 | 34 | 35 | 40 | 41 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_log.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 24 | 25 | 26 | 27 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_map.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_nmealog.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 24 | 25 | 26 | 27 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_statistics.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 20 | 21 | 26 | 27 | 32 | 33 | 39 | 40 | 48 | 49 | 60 | 61 | 62 | 67 | 68 | 76 | 77 | 88 | 89 | 90 | 96 | 97 | 105 | 106 | 117 | 118 | 119 | 124 | 125 | 133 | 134 | 144 | 145 | 146 | 151 | 152 | 160 | 161 | 171 | 172 | 173 | 179 | 180 | 188 | 189 | 199 | 200 | 201 | 206 | 207 | 215 | 216 | 226 | 227 | 228 | 233 | 234 | 242 | 243 | 253 | 254 | 255 | 260 | 261 | 269 | 270 | 280 | 281 | 282 | 287 | 288 | 296 | 297 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 17 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/menu/toolbar_menu.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 17 | 22 | 27 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvde-github/AIS-catcher-for-Android/0019885587eb547a39b38df98c77287fc1db13c2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-land/dimens.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 21 | 22 | 26 | 27 | 30 | 31 | 15 | 16 | 19 | 20 | 24 | 25 |