├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable-hdpi │ │ │ │ └── ic_stat.png │ │ │ ├── drawable-mdpi │ │ │ │ └── ic_stat.png │ │ │ ├── drawable-xhdpi │ │ │ │ └── ic_stat.png │ │ │ ├── drawable-xxhdpi │ │ │ │ └── ic_stat.png │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-xxxhdpi │ │ │ │ └── ic_stat.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── values │ │ │ │ ├── styles.xml │ │ │ │ ├── arrays.xml │ │ │ │ └── strings.xml │ │ │ ├── layout │ │ │ │ ├── dialog_aprsfilter.xml │ │ │ │ ├── fragment_maps.xml │ │ │ │ ├── rowlayout.xml │ │ │ │ └── dialog_aircraft.xml │ │ │ ├── menu │ │ │ │ ├── manageid_menu.xml │ │ │ │ └── main_menu.xml │ │ │ └── xml │ │ │ │ └── preferences.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── meisterschueler │ │ │ │ └── ognviewer │ │ │ │ ├── ui │ │ │ │ ├── AircraftDialogCallback.java │ │ │ │ ├── CustomAircraftDescriptorAdapter.java │ │ │ │ ├── AircraftDialog.java │ │ │ │ ├── ManageIDsFragment.java │ │ │ │ └── PrefsFragment.java │ │ │ │ ├── network │ │ │ │ └── flightpath │ │ │ │ │ ├── FlightPathApi.java │ │ │ │ │ ├── FlightPath.java │ │ │ │ │ └── AircraftPosition.java │ │ │ │ ├── common │ │ │ │ ├── entity │ │ │ │ │ ├── AircraftBundle.java │ │ │ │ │ ├── AircraftDataParcelable.java │ │ │ │ │ └── Aircraft.java │ │ │ │ ├── ReceiverBundle.java │ │ │ │ ├── AircraftDescriptorProviderHelper.java │ │ │ │ ├── Utils.java │ │ │ │ ├── FlarmMessagePFLAUSenderTask.java │ │ │ │ ├── AprsFilterManager.java │ │ │ │ ├── FlarmMessageSenderTask.java │ │ │ │ ├── AppConstants.java │ │ │ │ ├── FlarmMessage.java │ │ │ │ ├── CustomAircraftDescriptorProvider.java │ │ │ │ └── ReceiverBeaconImplReplacement.java │ │ │ │ ├── activity │ │ │ │ ├── base │ │ │ │ │ ├── KillBroadcastReceiver.java │ │ │ │ │ └── BaseActivity.java │ │ │ │ ├── ClosingActivity.java │ │ │ │ ├── PrefsActivity.java │ │ │ │ └── ManageIDsActivity.java │ │ │ │ ├── CustomAircraftDescriptor.java │ │ │ │ └── service │ │ │ │ ├── TcpServer.java │ │ │ │ └── OgnService.java │ │ └── AndroidManifest.xml │ ├── debug │ │ └── res │ │ │ └── values │ │ │ └── google_maps_api.xml │ ├── release │ │ └── res │ │ │ └── values │ │ │ └── google_maps_api.xml │ └── test │ │ └── java │ │ └── com │ │ └── meisterschueler │ │ └── ognviewer │ │ ├── common │ │ ├── UtilsTest.java │ │ └── FlarmMessageTest.java │ │ └── AprsFilterManagerTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── ic_stat.zip ├── logo_512.png ├── logo_1024x500.png ├── logo_512_abstand.png ├── logo_512_abstand.zip ├── ogn-logo-vector-2.eps ├── logo_512_transparent.png ├── res ├── drawable-hdpi │ └── ic_stat.png ├── drawable-mdpi │ └── ic_stat.png ├── drawable-xhdpi │ └── ic_stat.png ├── drawable-xxhdpi │ └── ic_stat.png └── drawable-xxxhdpi │ └── ic_stat.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── runConfigurations.xml ├── gradle.xml └── codeStyles │ └── Project.xml ├── gradle.properties ├── LICENSE ├── README.md ├── release-notes.md ├── .gitignore ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /ic_stat.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/ic_stat.zip -------------------------------------------------------------------------------- /logo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/logo_512.png -------------------------------------------------------------------------------- /logo_1024x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/logo_1024x500.png -------------------------------------------------------------------------------- /logo_512_abstand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/logo_512_abstand.png -------------------------------------------------------------------------------- /logo_512_abstand.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/logo_512_abstand.zip -------------------------------------------------------------------------------- /ogn-logo-vector-2.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/ogn-logo-vector-2.eps -------------------------------------------------------------------------------- /logo_512_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/logo_512_transparent.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/res/drawable-hdpi/ic_stat.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/res/drawable-mdpi/ic_stat.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/res/drawable-xhdpi/ic_stat.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/res/drawable-xxhdpi/ic_stat.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /res/drawable-xxxhdpi/ic_stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/res/drawable-xxxhdpi/ic_stat.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/app/src/main/res/drawable-hdpi/ic_stat.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/app/src/main/res/drawable-mdpi/ic_stat.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/app/src/main/res/drawable-xhdpi/ic_stat.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_stat.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/app/src/main/res/drawable-xxxhdpi/ic_stat.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meisterschueler/ogn-viewer-android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/ui/AircraftDialogCallback.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.ui; 2 | 3 | public interface AircraftDialogCallback { 4 | 5 | public void notifyUpdated(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 31 18:10:46 CET 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/network/flightpath/FlightPathApi.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.network.flightpath; 2 | 3 | import retrofit2.Call; 4 | import retrofit2.http.GET; 5 | import retrofit2.http.Path; 6 | 7 | public interface FlightPathApi { 8 | 9 | @GET("flightpath/{address}") 10 | Call getFlightPath(@Path("address") String address); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_aprsfilter.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/entity/AircraftBundle.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common.entity; 2 | 3 | import org.ogn.commons.beacon.AircraftBeacon; 4 | import org.ogn.commons.beacon.AircraftDescriptor; 5 | 6 | public class AircraftBundle { 7 | public AircraftBeacon aircraftBeacon; 8 | public AircraftDescriptor aircraftDescriptor; 9 | 10 | public AircraftBundle(AircraftBeacon aircraftBeacon, AircraftDescriptor aircraftDescriptor) { 11 | this.aircraftBeacon = aircraftBeacon; 12 | this.aircraftDescriptor = aircraftDescriptor; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/network/flightpath/FlightPath.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.network.flightpath; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.util.List; 6 | 7 | public class FlightPath { 8 | 9 | @SerializedName("address") 10 | private String address; 11 | 12 | @SerializedName("positions") 13 | private List positions; 14 | 15 | 16 | public String getAddress() { 17 | return address; 18 | } 19 | 20 | public List getPositions() { 21 | return positions; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_maps.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/ReceiverBundle.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | import org.ogn.commons.beacon.ReceiverBeacon; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class ReceiverBundle { 9 | public ReceiverBeacon receiverBeacon; 10 | public int beaconCount; 11 | static public int maxBeaconCounter; 12 | public List aircrafts; 13 | static public int maxAircraftCounter; 14 | 15 | public ReceiverBundle(ReceiverBeacon receiverBeacon) { 16 | this.receiverBeacon = receiverBeacon; 17 | 18 | this.beaconCount = 0; 19 | this.aircrafts = new ArrayList<>(); 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/res/menu/manageid_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 14 | 17 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/konstantin/Development/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/network/flightpath/AircraftPosition.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.network.flightpath; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class AircraftPosition { 6 | 7 | @SerializedName("latitude") 8 | private double latitude; 9 | 10 | @SerializedName("longitude") 11 | private double longitude; 12 | 13 | @SerializedName("altitudeInMeters") 14 | private double altitudeInMeters; 15 | 16 | 17 | public double getLatitude() { 18 | return latitude; 19 | } 20 | 21 | public double getLongitude() { 22 | return longitude; 23 | } 24 | 25 | public double getAltitudeInMeters() { 26 | return altitudeInMeters; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 12 | 13 | 16 | 17 | 20 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/activity/base/KillBroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.activity.base; 2 | 3 | import android.app.Activity; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | 8 | public class KillBroadcastReceiver extends BroadcastReceiver{ 9 | // idea from https://stackoverflow.com/questions/5453206/how-to-close-all-the-activities-of-my-application/5453228#5453228 10 | Activity callbackActivity; 11 | 12 | public KillBroadcastReceiver(Activity activity) { 13 | this.callbackActivity = activity; 14 | } 15 | 16 | @Override 17 | public void onReceive(Context context, Intent intent) { 18 | if (callbackActivity != null) { 19 | callbackActivity.finish(); 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/debug/res/values/google_maps_api.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | AIzaSyClTQaIqL0dM-hPs7stZZkWxCiILUmrVQ0 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/release/res/values/google_maps_api.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | AIzaSyAOVkp9UR_gKUeHDrLaRx71bOp9rfXPjL4 17 | 18 | 19 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | android.enableJetifier=true 20 | android.useAndroidX=true 21 | org.gradle.jvmargs=-Xmx1536M -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/entity/AircraftDataParcelable.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common.entity; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | /** 7 | * Created by Dominik on 20.08.2017. 8 | */ 9 | 10 | public class AircraftDataParcelable implements Parcelable { 11 | 12 | protected AircraftDataParcelable(Parcel in) { 13 | } 14 | 15 | public static final Creator CREATOR = new Creator() { 16 | @Override 17 | public AircraftDataParcelable createFromParcel(Parcel in) { 18 | return new AircraftDataParcelable(in); 19 | } 20 | 21 | @Override 22 | public AircraftDataParcelable[] newArray(int size) { 23 | return new AircraftDataParcelable[size]; 24 | } 25 | }; 26 | 27 | @Override 28 | public int describeContents() { 29 | return 0; 30 | } 31 | 32 | @Override 33 | public void writeToParcel(Parcel dest, int flags) { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/test/java/com/meisterschueler/ognviewer/common/UtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | import com.meisterschueler.ognviewer.common.Utils; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | public class UtilsTest { 9 | @Test 10 | public void getHue() { 11 | float hue = Utils.getHue(0, 0, 100, 120, 360); 12 | Assert.assertEquals(hue, 120, 0.1); 13 | 14 | hue = Utils.getHue(4, 1, 7, 90, 180); 15 | Assert.assertEquals(hue, 135, 0.1); 16 | } 17 | 18 | @Test 19 | public void getHue_exceed() { 20 | float hue = Utils.getHue(1, 2, 3, 0, 90); 21 | Assert.assertEquals(hue, 0, 0.1); 22 | 23 | hue = Utils.getHue(2, 0, 1, 0, 90); 24 | Assert.assertEquals(hue, 90, 0.1); 25 | } 26 | 27 | @Test 28 | public void getHue_invalid() { 29 | float hue = Utils.getHue(1, 0, 0, 0, 360); 30 | Assert.assertEquals(hue, 0, 0.1); 31 | 32 | hue = Utils.getHue(1, 0, 1, 0, 360); 33 | Assert.assertEquals(hue, 0, 0.1); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/activity/base/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.activity.base; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.IntentFilter; 5 | import android.os.Bundle; 6 | 7 | import androidx.annotation.Nullable; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | 10 | import com.meisterschueler.ognviewer.common.AppConstants; 11 | 12 | @SuppressLint("Registered") 13 | public class BaseActivity extends AppCompatActivity { 14 | 15 | KillBroadcastReceiver killBroadcastReceiver; 16 | 17 | 18 | @Override 19 | protected void onCreate(@Nullable Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | 22 | killBroadcastReceiver = new KillBroadcastReceiver(this); 23 | registerReceiver(killBroadcastReceiver, new IntentFilter(AppConstants.EMERGENCY_EXIT_INTENT_ACTION_NAME)); 24 | } 25 | 26 | @Override 27 | protected void onDestroy() { 28 | super.onDestroy(); 29 | 30 | unregisterReceiver(killBroadcastReceiver); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/AircraftDescriptorProviderHelper.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | 4 | import org.ogn.client.OgnClient; 5 | import org.ogn.client.OgnClientFactory; 6 | import org.ogn.commons.beacon.descriptor.AircraftDescriptorProvider; 7 | import org.ogn.commons.db.FileDbDescriptorProvider; 8 | import org.ogn.commons.db.ogn.OgnDb; 9 | 10 | import java.util.Arrays; 11 | 12 | public enum AircraftDescriptorProviderHelper { 13 | INSTANCE; 14 | 15 | private static final AircraftDescriptorProvider adp1 = new CustomAircraftDescriptorProvider(); 16 | private static final AircraftDescriptorProvider adp2 = new FileDbDescriptorProvider<>(OgnDb.class); 17 | 18 | public static final AircraftDescriptorProvider getCustomDbAircraftDescriptorProvider() { 19 | return adp1; 20 | } 21 | 22 | public static final AircraftDescriptorProvider getOgnDbAircraftDescriptorProvider() { 23 | return adp2; 24 | } 25 | 26 | public static final OgnClient getOgnClient(String serverName) { 27 | //String versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; //TODO: find a workaround 2018-08-26 28 | return OgnClientFactory.getBuilder().descriptorProviders(Arrays.asList(adp1, adp2)) 29 | .serverName(serverName).appName("ogn-viewer").appVersion("1.4.5").build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/activity/ClosingActivity.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.activity; 2 | 3 | import android.app.Activity; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.ServiceConnection; 8 | import android.os.IBinder; 9 | 10 | import com.meisterschueler.ognviewer.common.AppConstants; 11 | import com.meisterschueler.ognviewer.service.OgnService; 12 | 13 | public class ClosingActivity extends Activity { 14 | 15 | private ServiceConnection mConnection = new ServiceConnection() { 16 | public void onServiceConnected(ComponentName className, IBinder binder) { 17 | stopService(new Intent(getBaseContext(), OgnService.class)); 18 | Intent exitIntent = new Intent(AppConstants.EMERGENCY_EXIT_INTENT_ACTION_NAME); 19 | sendBroadcast(exitIntent); 20 | finish(); 21 | } 22 | 23 | public void onServiceDisconnected(ComponentName className) { 24 | //this only happens when something goes wrong 25 | //it does not happen when activity is paused or destroyed 26 | finish(); 27 | } 28 | }; 29 | 30 | @Override 31 | protected void onResume() { 32 | super.onResume(); 33 | bindService(new Intent(this, OgnService.class), mConnection, Context.BIND_AUTO_CREATE); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/Utils.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | public class Utils { 4 | private final static float METERS_TO_FEET = 3.2808398950131f; 5 | private final static float KMH_TO_MPH = 0.62137119223733f; 6 | private final static float KMH_TO_KT = 0.53995680346039f; 7 | private final static float MS_TO_FPM = 196.8504f; 8 | 9 | public static float getHue(float value, float min, float max, int minColor, int maxColor) { 10 | float hue; 11 | if (min == max || value <= min) { 12 | hue = minColor; 13 | } else if (value > max) { 14 | hue = maxColor; 15 | } else { 16 | float colorValue = (value - min) / (max - min); // from 0.0 to 1.0 17 | hue = minColor + colorValue * (maxColor - minColor); 18 | } 19 | 20 | float result = hue % 360.0f; 21 | if (result >= 360.0 || result < 0) { 22 | result = 0; 23 | } 24 | 25 | return result; 26 | } 27 | 28 | public static float metersToFeet(float meters) { 29 | return meters * METERS_TO_FEET; 30 | } 31 | 32 | public static float kmhToMph(float kmh) { 33 | return kmh * KMH_TO_MPH; 34 | } 35 | 36 | public static float kmhToKt(float kmh) { 37 | return kmh * KMH_TO_KT; 38 | } 39 | 40 | public static float msToFpm(float ms) { 41 | return ms * MS_TO_FPM; 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/rowlayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 15 | 21 | 22 | 23 | 29 | 30 | 31 | 37 | 38 | 39 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/test/java/com/meisterschueler/ognviewer/AprsFilterManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer; 2 | 3 | import com.meisterschueler.ognviewer.common.AprsFilterManager; 4 | 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | import static org.junit.Assert.assertNull; 9 | 10 | public class AprsFilterManagerTest { 11 | 12 | private static final double DELTA = 1e-15; 13 | 14 | @Test 15 | public void simpleIntegers() { 16 | AprsFilterManager.Circle result = AprsFilterManager.parse("r/52/13/100"); 17 | assertEquals(result.getLat(), 52, DELTA); 18 | assertEquals(result.getLon(), 13, DELTA); 19 | assertEquals(result.getRadius(), 100, DELTA); 20 | } 21 | 22 | @Test 23 | public void simpleFloats() { 24 | AprsFilterManager.Circle result = AprsFilterManager.parse("r/+52.513/-13.500/100.041"); 25 | assertEquals(result.getLat(), 52.513, DELTA); 26 | assertEquals(result.getLon(), -13.500, DELTA); 27 | assertEquals(result.getRadius(), 100.041, DELTA); 28 | } 29 | 30 | @Test 31 | public void uglyFloats() { 32 | AprsFilterManager.Circle result = AprsFilterManager.parse("r/+052./-.500/100"); 33 | assertEquals(result.getLat(), 52.0, DELTA); 34 | assertEquals(result.getLon(), -0.500, DELTA); 35 | assertEquals(result.getRadius(), 100.0, DELTA); 36 | } 37 | 38 | @Test 39 | public void invalidFloat() { 40 | AprsFilterManager.Circle result = AprsFilterManager.parse("r//13/100"); 41 | assertNull(result); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion "28.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.meisterschueler.ognviewer" 9 | minSdkVersion 19 10 | targetSdkVersion 29 11 | versionCode 47 12 | versionName "1.4.5" 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation fileTree(include: ['*.jar'], dir: 'libs') 25 | implementation 'com.google.android.gms:play-services-maps:17.0.0' 26 | implementation 'com.google.android.gms:play-services-location:17.0.0' 27 | implementation 'org.ogn:ogn-commons-java:2.0.0-SNAPSHOT' 28 | implementation 'org.ogn:ogn-client-java:1.0.0-SNAPSHOT' 29 | implementation 'com.google.maps.android:android-maps-utils:0.5' 30 | implementation 'co.uk.rushorm:rushandroid:1.2.0' // leave at 1.2.0 to avoid issues 2018-05-15 31 | testImplementation 'junit:junit:4.12' 32 | testImplementation 'org.robolectric:robolectric:4.2.1' 33 | implementation 'com.jakewharton.timber:timber:4.7.1' 34 | implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0" 35 | implementation "androidx.preference:preference:1.1.0" 36 | 37 | implementation 'com.squareup.retrofit2:retrofit:2.5.0' 38 | implementation 'com.squareup.retrofit2:converter-gson:2.5.0' 39 | 40 | implementation "org.apache.commons:commons-csv:1.5" 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/FlarmMessagePFLAUSenderTask.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import java.io.DataOutputStream; 6 | import java.io.IOException; 7 | import java.net.Socket; 8 | 9 | 10 | public class FlarmMessagePFLAUSenderTask extends AsyncTask { 11 | 12 | // This sender task sends a PFLAU message. This messages shows that the "flarm" is alive. 13 | // See here: https://github.com/Meisterschueler/ogn-viewer-android/issues/17 14 | 15 | private Socket clientSocket; 16 | 17 | private FlarmMessagePFLAUSenderTask() { 18 | // don't allow instances without params 19 | } 20 | 21 | public FlarmMessagePFLAUSenderTask(Socket clientSocket) { 22 | this.clientSocket = clientSocket; 23 | } 24 | 25 | @Override 26 | protected Void doInBackground(Object[] objects) { 27 | DataOutputStream objectOutput; 28 | 29 | try { 30 | objectOutput = new DataOutputStream(clientSocket.getOutputStream()); 31 | } catch (IOException e) { 32 | e.printStackTrace(); 33 | return null; 34 | } 35 | 36 | String message = "$PFLAU,0,1,1,1,0,,0,,*63"; 37 | 38 | try { 39 | objectOutput.write((message + "\r\n").getBytes("US-ASCII")); 40 | } catch (IOException e) { 41 | //e.printStackTrace(); 42 | try { 43 | clientSocket.close(); 44 | } catch (IOException e1) { 45 | e1.printStackTrace(); 46 | } 47 | } 48 | 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/AprsFilterManager.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | import java.util.Locale; 4 | import java.util.regex.Matcher; 5 | import java.util.regex.Pattern; 6 | 7 | public class AprsFilterManager { 8 | public static Circle parse(String aprs_filter) { 9 | String re_float = "[+-]?((\\d+\\.?\\d*)|(\\.\\d+))"; 10 | String re_range = "^r/(" + re_float + ")/(" + re_float + ")/(" + re_float + ")$"; 11 | 12 | Pattern pattern = Pattern.compile(re_range); 13 | Matcher matcher = pattern.matcher(aprs_filter); 14 | 15 | Circle result = null; 16 | if (matcher.matches()) { 17 | result = new Circle(); 18 | result.lat = Double.parseDouble(matcher.group(1)); 19 | result.lon = Double.parseDouble(matcher.group(5)); 20 | result.radius = Double.parseDouble(matcher.group(9)); 21 | } 22 | 23 | return result; 24 | } 25 | 26 | public static String latLngToAprsFilter(double lat, double lon, double radius) { 27 | return String.format(Locale.US, "r/%1$.3f/%2$.3f/%3$.1f", lat, lon, radius/1000.0); 28 | } 29 | 30 | public static String latLngToAprsFilter(double lat, double lon) { 31 | return latLngToAprsFilter(lat, lon, AppConstants.DEFAULT_APRS_FILTER_RADIUS); 32 | } 33 | 34 | public static class Circle { 35 | double lat; 36 | double lon; 37 | double radius; 38 | 39 | 40 | public double getLat() { 41 | return lat; 42 | } 43 | 44 | public double getLon() { 45 | return lon; 46 | } 47 | 48 | public double getRadius() { 49 | return radius; 50 | } 51 | 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/activity/PrefsActivity.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.activity; 2 | 3 | import android.content.Intent; 4 | import android.content.pm.PackageManager; 5 | import android.os.Bundle; 6 | 7 | import com.meisterschueler.ognviewer.activity.base.BaseActivity; 8 | import com.meisterschueler.ognviewer.common.AppConstants; 9 | import com.meisterschueler.ognviewer.ui.PrefsFragment; 10 | 11 | import timber.log.Timber; 12 | 13 | public class PrefsActivity extends BaseActivity { 14 | 15 | PrefsFragment prefsFragment; 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | 21 | prefsFragment = new PrefsFragment(); 22 | getSupportFragmentManager().beginTransaction() 23 | .replace(android.R.id.content, prefsFragment) 24 | .commit(); 25 | 26 | Intent intent = new Intent(); 27 | intent.putExtra("MESSAGE","Prefs Activity finished"); 28 | setResult(AppConstants.ACTIVITY_REQUEST_CODE_SETTINGS, intent); 29 | } 30 | 31 | @Override 32 | public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { 33 | switch (requestCode) { 34 | case AppConstants.REQUEST_CODE_LOCATION_TCP_UPDATES: { 35 | // If request is cancelled, the result arrays are empty. 36 | if (grantResults.length > 0 37 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 38 | prefsFragment.setTCPServerActiveState(true); 39 | } else { 40 | Timber.d("Location permission for TCP Server denied"); 41 | prefsFragment.setTCPServerActiveState(false); 42 | } 43 | break; 44 | } 45 | 46 | // other 'case' lines to check for other 47 | // permissions this app might request. 48 | } 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/activity/ManageIDsActivity.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.activity; 2 | 3 | import android.content.pm.PackageManager; 4 | import android.os.Bundle; 5 | 6 | import com.meisterschueler.ognviewer.activity.base.BaseActivity; 7 | import com.meisterschueler.ognviewer.common.AppConstants; 8 | import com.meisterschueler.ognviewer.ui.ManageIDsFragment; 9 | 10 | public class ManageIDsActivity extends BaseActivity { 11 | 12 | private ManageIDsFragment manageIDsActivity; 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | 18 | manageIDsActivity = new ManageIDsFragment(); 19 | 20 | getSupportFragmentManager().beginTransaction() 21 | .replace(android.R.id.content, manageIDsActivity) 22 | .commit(); 23 | } 24 | 25 | @Override 26 | public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { 27 | switch (requestCode) { 28 | case AppConstants.REQUEST_CODE_STORAGE_IMPORT: { 29 | // If request is cancelled, the result arrays are empty. 30 | if (grantResults.length > 0 31 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 32 | // permission was granted, yay! 33 | manageIDsActivity.importItems(); 34 | } 35 | break; 36 | } 37 | case AppConstants.REQUEST_CODE_STORAGE_EXPORT: { 38 | // If request is cancelled, the result arrays are empty. 39 | if (grantResults.length > 0 40 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 41 | // permission was granted, yay! 42 | manageIDsActivity.exportItems(); 43 | } 44 | break; 45 | } 46 | 47 | // other 'case' lines to check for other 48 | // permissions this app might request. 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OGN Viewer - FLARM Radar 2 | 3 | ## Description 4 | Android app that visualizes the aircraft traffic from [The Open Glider Network project (OGN)](http://glidernet.org) on a map. 5 | 6 | OGN can receive signals from gliders, tow planes, helicopters, etc. 7 | 8 | This app is a flight radar for small aircraft with FLARM equipment. 9 | 10 | The decoded signals can be used with apps like XCSoar on TCP port 4353. 11 | 12 | Current version: 1.4.4 13 | 14 | See [release notes](release-notes.md) for details. 15 | 16 | Download the app at https://play.google.com/store/apps/details?id=com.meisterschueler.ognviewer 17 | 18 | 19 | ## Features 20 | Track aircraft with FLARM devices via the OGN and see OGN receivers on a map. 21 | 22 | See flight paths of aircraft - track only (default) or with heights (multicolor option in settings). 23 | 24 | Use APRS filter for filtering oder long click on the map to set a radius filter. 25 | 26 | App can run in background and listen to OGN messages. Use exit function to save battery and data! 27 | 28 | Other apps can connect on TCP port 4353 (must be activated in settings). 29 | 30 | Manage known aircraft in "ManageIDs" activity or click long on the info window of an aircraft. 31 | 32 | 33 | ## Dependencies 34 | The app uses two repositories from Meisterschueler: 35 | 36 | https://github.com/Meisterschueler/ogn-commons-java 37 | 38 | and 39 | 40 | https://github.com/Meisterschueler/ogn-client-java 41 | 42 | Clone the Java7 files from "Android" branch! 43 | 44 | You need Eclipse and Maven to build the jar files. 45 | 46 | Use "maven install" to build them. 47 | 48 | 49 | ## Building 50 | Use Android Studio to build the app. 51 | 52 | Replace the google_maps_key in google_maps_api.xml with your own values. 53 | 54 | Go to https://console.developer.google.com and sign in with your credentials. 55 | 56 | Add a new project for ogn-viewer. 57 | 58 | Generate a new api key and copy it to the xml file. 59 | 60 | Add the package name "com.meisterschueler.ognviewer" to your api key. 61 | 62 | You need your own SHA1 fingerprint too. 63 | 64 | Use `keytool -list -v -keystore mykeystorepath.keystore` to get it. 65 | 66 | 67 | -------------------------------------------------------------------------------- /release-notes.md: -------------------------------------------------------------------------------- 1 | # OGN Viewer Release Notes 2 | 3 | ## 1.4.4 4 | * Improvement: Updated ogn library (reduces power consumption) 5 | * Bugfix: Crash (issue #25) 6 | 7 | ## 1.4.3 8 | * New: moving filter (issue #9) 9 | * Enhancement: allow custom APRS server 10 | 11 | ## 1.4.2 12 | * Bugfix: Crash with bad APRS filter (issue #20) 13 | 14 | ## 1.4.1 15 | * Improved: TCP server also available for LK8000 (issue #17) 16 | 17 | ## 1.4.0 18 | * New: import/export IDs (file format: csv) 19 | * Rised minimum API level from 15 to 19 (Android 4.0.4 -> 4.4) 20 | 21 | ## 1.3.9 22 | * New: multicolor flight path (see settings) 23 | * New: screen orientation setting 24 | * Fixed: jumping aircraft position (issue #14) 25 | * Fixed: black screen issue (while restoring map) 26 | * Changed: info window click (short and long) 27 | * improved manageIDs activity (delete function on long click) 28 | * many internal improvements and refactorings 29 | 30 | ## 1.3.8 31 | * New: flight paths of aircraft (must be enabled in settings) 32 | * New: (emergency) stop button at notification 33 | 34 | ## 1.3.7 35 | * Fixed: small bugs (issue #10 and #11) 36 | 37 | ## 1.3.6 38 | * Fixed: GPS issues 39 | * Fixed: inactive aircraft timer issue - Please readjust your timeout setting! 40 | * Fixed: info window could hide aircraft marker when aircraft rotated 41 | * New: units can be changed 42 | * New: TCP server updates on/off setting 43 | 44 | ## 1.3.5 45 | * Fixed: notification not visible on Android 7 and up 46 | * Fixed: app crash at start on Android 8.1 47 | 48 | ## 1.3.4 49 | * Fixed: bugs from receiver markers 50 | * Fixed: OutOfMemory error with many markers 51 | * Fixed: wrong behaviour with empty filter 52 | * New: info window shows time of last signal from aircraft and receivers 53 | * New: user defined timeout for inactive aircraft 54 | * New: user defined keep screen on function 55 | * Known issue: GPS function doesn't work on all devices 56 | 57 | ## 1.3.3 58 | * Fixed NPE for GPS access 59 | * Known issue: GPS function doesn't work on all devices 60 | 61 | ## 1.3.2 62 | * massive speed improvement 63 | * map type can be configured 64 | * aircraft markers can be rotated 65 | * fixed several bugs 66 | 67 | ## 1.3.1 68 | * added tcp server 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/CustomAircraftDescriptor.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer; 2 | 3 | import org.ogn.commons.beacon.AircraftDescriptor; 4 | 5 | import co.uk.rushorm.core.RushObject; 6 | 7 | /** 8 | * CAUTION: moving to other package requires DB migration!!! 9 | */ 10 | public class CustomAircraftDescriptor extends RushObject { 11 | public String address = ""; 12 | 13 | public String regNumber = ""; 14 | public String CN = ""; 15 | public String owner = ""; 16 | public String homeBase = ""; 17 | public String model = ""; 18 | public String freq = ""; 19 | 20 | boolean favourite = false; 21 | 22 | public CustomAircraftDescriptor() { 23 | } 24 | 25 | public CustomAircraftDescriptor(String address, AircraftDescriptor ad) { 26 | this.address = address; 27 | 28 | this.regNumber = ad.getRegNumber(); 29 | this.CN = ad.getCN(); 30 | this.owner = ad.getOwner(); 31 | this.homeBase = ad.getHomeBase(); 32 | this.model = ad.getModel(); 33 | this.freq = ad.getFreq(); 34 | } 35 | 36 | public CustomAircraftDescriptor(String address, String regNumber, String CN, String owner, String homeBase, String model, String freq) { 37 | this.address = address; 38 | 39 | this.regNumber = regNumber; 40 | this.CN = CN; 41 | this.owner = owner; 42 | this.homeBase = homeBase; 43 | this.model = model; 44 | this.freq = freq; 45 | } 46 | 47 | public CustomAircraftDescriptor(String csv) { 48 | String[] parts = csv.split(","); 49 | if (parts.length == 5) { 50 | this.address = parts[0]; 51 | 52 | this.regNumber = parts[1]; 53 | this.CN = parts[2]; 54 | this.owner = parts[3]; 55 | this.model = parts[4]; 56 | } 57 | } 58 | 59 | public String toCsv() { 60 | return this.address + "," + this.regNumber + "," + this.CN + "," + this.owner + "," + this.model; 61 | } 62 | 63 | public boolean isEmpty() { 64 | return (regNumber.isEmpty() && CN.isEmpty() && owner.isEmpty() && homeBase.isEmpty() && model.isEmpty() && freq.isEmpty()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/ui/CustomAircraftDescriptorAdapter.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.ui; 2 | 3 | 4 | import android.content.Context; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ArrayAdapter; 9 | import android.widget.TextView; 10 | 11 | import com.meisterschueler.ognviewer.R; 12 | import com.meisterschueler.ognviewer.CustomAircraftDescriptor; 13 | 14 | public class CustomAircraftDescriptorAdapter extends ArrayAdapter { 15 | 16 | private final Context context; 17 | private final CustomAircraftDescriptor[] values; 18 | 19 | public CustomAircraftDescriptorAdapter(Context context, CustomAircraftDescriptor[] values) { 20 | super(context, R.layout.rowlayout, values); 21 | this.context = context; 22 | this.values = values; 23 | } 24 | 25 | @Override 26 | public View getView(int position, View convertView, ViewGroup parent) { 27 | LayoutInflater inflater = (LayoutInflater) context 28 | .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 29 | View rowView = inflater.inflate(R.layout.rowlayout, parent, false); 30 | TextView tvAddress = rowView.findViewById(R.id.column_address); 31 | TextView tvRegNumber = rowView.findViewById(R.id.column_regNumber); 32 | TextView tvCN = rowView.findViewById(R.id.column_CN); 33 | TextView tvModel = rowView.findViewById(R.id.column_model); 34 | TextView tvOwner = rowView.findViewById(R.id.column_owner); 35 | //ImageView imageView = (ImageView) rowView.findViewById(R.id.icon); 36 | 37 | CustomAircraftDescriptor cad = values[position]; 38 | tvAddress.setText(cad.address); 39 | tvRegNumber.setText(cad.regNumber); 40 | tvCN.setText(cad.CN); 41 | tvModel.setText(cad.model); 42 | tvOwner.setText(cad.owner); 43 | // Change the icon for Windows and iPhone 44 | /*String s = values[position]; 45 | if (s.startsWith("Windows7") || s.startsWith("iPhone") 46 | || s.startsWith("Solaris")) { 47 | imageView.setImageResource(R.drawable.no); 48 | } else { 49 | imageView.setImageResource(R.drawable.ok); 50 | }*/ 51 | 52 | return rowView; 53 | } 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_aircraft.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 21 | 22 | 28 | 29 | 36 | 37 | 44 | 45 | 52 | 53 | 61 | 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/androidstudio 3 | 4 | ### AndroidStudio ### 5 | # Covers files to be ignored for android development using Android Studio. 6 | 7 | # Built application files 8 | *.apk 9 | *.ap_ 10 | 11 | # Files for the ART/Dalvik VM 12 | *.dex 13 | 14 | # Java class files 15 | *.class 16 | 17 | # Generated files 18 | bin/ 19 | gen/ 20 | out/ 21 | 22 | # Gradle files 23 | .gradle 24 | .gradle/ 25 | build/ 26 | 27 | # Signing files 28 | .signing/ 29 | 30 | # Local configuration file (sdk path, etc) 31 | local.properties 32 | 33 | # Proguard folder generated by Eclipse 34 | proguard/ 35 | 36 | # Log Files 37 | *.log 38 | 39 | # Android Studio 40 | /*/build/ 41 | /*/local.properties 42 | /*/out 43 | /*/*/build 44 | /*/*/production 45 | captures/ 46 | .navigation/ 47 | *.ipr 48 | *~ 49 | *.swp 50 | 51 | # Android Patch 52 | gen-external-apklibs 53 | 54 | # External native build folder generated in Android Studio 2.2 and later 55 | .externalNativeBuild 56 | 57 | # NDK 58 | obj/ 59 | 60 | # IntelliJ IDEA 61 | *.iml 62 | *.iws 63 | /out/ 64 | 65 | # User-specific configurations 66 | .idea/caches/ 67 | .idea/libraries/ 68 | .idea/shelf/ 69 | .idea/workspace.xml 70 | .idea/tasks.xml 71 | .idea/.name 72 | .idea/compiler.xml 73 | .idea/copyright/profiles_settings.xml 74 | .idea/encodings.xml 75 | .idea/misc.xml 76 | .idea/modules.xml 77 | .idea/scopes/scope_settings.xml 78 | .idea/dictionaries 79 | .idea/vcs.xml 80 | .idea/jsLibraryMappings.xml 81 | .idea/datasources.xml 82 | .idea/dataSources.ids 83 | .idea/sqlDataSources.xml 84 | .idea/dynamic.xml 85 | .idea/uiDesigner.xml 86 | 87 | # OS-specific files 88 | .DS_Store 89 | .DS_Store? 90 | ._* 91 | .Spotlight-V100 92 | .Trashes 93 | ehthumbs.db 94 | Thumbs.db 95 | 96 | # Legacy Eclipse project files 97 | .classpath 98 | .project 99 | .cproject 100 | .settings/ 101 | 102 | # Mobile Tools for Java (J2ME) 103 | .mtj.tmp/ 104 | 105 | # Package Files # 106 | *.war 107 | *.ear 108 | 109 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 110 | hs_err_pid* 111 | 112 | ## Plugin-specific files: 113 | 114 | # mpeltonen/sbt-idea plugin 115 | .idea_modules/ 116 | 117 | # JIRA plugin 118 | atlassian-ide-plugin.xml 119 | 120 | # Mongo Explorer plugin 121 | .idea/mongoSettings.xml 122 | 123 | # Crashlytics plugin (for Android Studio and IntelliJ) 124 | com_crashlytics_export_strings.xml 125 | crashlytics.properties 126 | crashlytics-build.properties 127 | fabric.properties 128 | 129 | ### AndroidStudio Patch ### 130 | 131 | !/gradle/wrapper/gradle-wrapper.jar 132 | 133 | 134 | # End of https://www.gitignore.io/api/androidstudio 135 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/FlarmMessageSenderTask.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | import android.location.Location; 4 | import android.os.AsyncTask; 5 | 6 | import java.io.DataOutputStream; 7 | import java.io.IOException; 8 | import java.net.Socket; 9 | import java.util.Calendar; 10 | import java.util.Iterator; 11 | import java.util.Map; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | 14 | 15 | public class FlarmMessageSenderTask extends AsyncTask { 16 | private static final long MAX_TIME = 30000; // keep flarm position for 30s 17 | private static final int MIN_DISTANCE = 2000; // if distance to flarm is < 2000m it wont be shown 18 | private Socket clientSocket; 19 | private Map messageMap = new ConcurrentHashMap<>(); 20 | private Location ownLocation; 21 | 22 | private FlarmMessageSenderTask() { 23 | // don't allow instances without params 24 | } 25 | 26 | public FlarmMessageSenderTask(Socket clientSocket, Map messageMap, Location ownLocation) { 27 | this.clientSocket = clientSocket; 28 | this.messageMap = messageMap; 29 | this.ownLocation = ownLocation; 30 | } 31 | 32 | @Override 33 | protected Void doInBackground(Object[] objects) { 34 | long time = Calendar.getInstance().getTime().getTime(); 35 | DataOutputStream objectOutput; 36 | 37 | try { 38 | objectOutput = new DataOutputStream(clientSocket.getOutputStream()); 39 | } catch (IOException e) { 40 | e.printStackTrace(); 41 | return null; 42 | } 43 | 44 | for(Iterator> it = messageMap.entrySet().iterator(); it.hasNext(); ) { 45 | Map.Entry entry = it.next(); 46 | if(time - entry.getValue().getTime() > MAX_TIME) { 47 | it.remove(); 48 | } else { 49 | FlarmMessage flarmMessage = entry.getValue(); 50 | flarmMessage.setOwnLocation(ownLocation); 51 | if (flarmMessage.getDistance() < MIN_DISTANCE) { 52 | continue; 53 | } 54 | String message = flarmMessage.toString(); 55 | try { 56 | objectOutput.write((message + "\r\n").getBytes("US-ASCII")); 57 | } catch (IOException e) { 58 | //e.printStackTrace(); 59 | try { 60 | clientSocket.close(); 61 | } catch (IOException e1) { 62 | e1.printStackTrace(); 63 | } 64 | } 65 | } 66 | } 67 | return null; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/AppConstants.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | 4 | public class AppConstants { 5 | // permission request codes (must be 0 to 65535 for AppCompat!) 6 | // so keep it unsigned short: https://stackoverflow.com/questions/33331073/android-what-to-choose-for-requestcode-values/33331459#33331459 7 | public static final int REQUEST_CODE_LOCATION_ZOOM = 54321; 8 | public static final int REQUEST_CODE_LOCATION_FILTER = 2468; 9 | public static final int REQUEST_CODE_LOCATION_TCP_UPDATES = 12341; 10 | public static final int REQUEST_CODE_LOCATION_TCP_UPDATES_FROM_SERVICE = 1234; 11 | public static final int REQUEST_CODE_STORAGE_IMPORT = 20201; 12 | public static final int REQUEST_CODE_STORAGE_EXPORT = 20202; 13 | 14 | // Intents 15 | public static final String INTENT_AIRCRAFT_BEACON = "AIRCRAFT-BEACON"; 16 | public static final String INTENT_RECEIVER_BEACON = "RECEIVER-BEACON"; 17 | public static final String INTENT_AIRCRAFT_ACTION = "AIRCRAFT_ACTION"; 18 | public static final String INTENT_LOCATION = "LOCATION"; 19 | 20 | /** 21 | * after this time inactive aircraft are removed 22 | */ 23 | public static final int DEFAULT_AIRCRAFT_TIMEOUT_IN_SEC = 300; 24 | 25 | /** 26 | * zoom factor for "go to current location" 27 | */ 28 | public static final int DEFAULT_MAP_ZOOM = 7; 29 | 30 | /** 31 | * request code for settings activity 32 | */ 33 | public static final int ACTIVITY_REQUEST_CODE_SETTINGS = 2000; 34 | 35 | /** 36 | * TCP port for other apps to connect 37 | */ 38 | public static final int TCP_SERVER_PORT = 4353; 39 | 40 | /** 41 | * REST API base url for flightpath 42 | */ 43 | public static final String FLIGHTPATH_API_BASE_URL = "http://ogn.dominik-p.de:18820/api/"; 44 | 45 | /** 46 | * restore markers after a little delay to prevent black screen issue 47 | */ 48 | public static final int RESTORE_MAP_AFTER_DELAY_IN_MS = 700; 49 | 50 | /** 51 | * minimal time between beacons for the same aircraft in MS 52 | */ 53 | public static final int MINIMAL_AIRCRAFT_DIFF_TIME_IN_MS = 500; 54 | 55 | /** 56 | * action name for emergency exit intent 57 | */ 58 | public static final String EMERGENCY_EXIT_INTENT_ACTION_NAME = "EMERGENCY_EXIT"; 59 | 60 | /** 61 | * minimal value for coloration of altitude in meters 62 | */ 63 | public static final float MIN_ALT_FOR_COLORATION = 500.0f; 64 | 65 | /** 66 | * maximal value for coloration of altitude in meters 67 | */ 68 | public static final float MAX_ALT_FOR_COLORATION = 3000.0f; 69 | 70 | /** 71 | * default aprs filter radius in m 72 | */ 73 | public static final float DEFAULT_APRS_FILTER_RADIUS = 100000.0f; 74 | 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/service/TcpServer.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.service; 2 | 3 | 4 | import android.location.Location; 5 | 6 | import com.meisterschueler.ognviewer.common.AppConstants; 7 | import com.meisterschueler.ognviewer.common.FlarmMessage; 8 | import com.meisterschueler.ognviewer.common.FlarmMessagePFLAUSenderTask; 9 | import com.meisterschueler.ognviewer.common.FlarmMessageSenderTask; 10 | 11 | import java.io.IOException; 12 | import java.net.ServerSocket; 13 | import java.net.Socket; 14 | import java.util.Map; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | 17 | public class TcpServer { 18 | private Thread serverThread; 19 | private ServerSocket serverSocket; 20 | private Socket clientSocket = null; 21 | private boolean stopped = true; 22 | private Map messageMap = new ConcurrentHashMap<>(); 23 | 24 | public void startServer() { 25 | stopped = false; 26 | Runnable serverTask = new Runnable() { 27 | @Override 28 | public void run() { 29 | try { 30 | serverSocket = new ServerSocket(AppConstants.TCP_SERVER_PORT); 31 | } catch (IOException e) { 32 | e.printStackTrace(); 33 | } 34 | 35 | while (!stopped && serverSocket != null) { 36 | if (clientSocket == null || clientSocket.isClosed()) { 37 | try { 38 | clientSocket = serverSocket.accept(); 39 | //playNMEA(); 40 | } catch (IOException e) { 41 | e.printStackTrace(); 42 | } 43 | } else { 44 | try { 45 | new FlarmMessagePFLAUSenderTask(clientSocket).execute(); 46 | Thread.sleep(1000); 47 | } catch (InterruptedException e) { 48 | e.printStackTrace(); 49 | } 50 | } 51 | } 52 | } 53 | }; 54 | serverThread = new Thread(serverTask); 55 | serverThread.start(); 56 | } 57 | 58 | public void stopServer() { 59 | stopped = true; 60 | if (clientSocket != null) { 61 | try { 62 | clientSocket.close(); 63 | 64 | } catch (IOException e) { 65 | e.printStackTrace(); 66 | } 67 | } 68 | if (serverThread != null) { 69 | serverThread.interrupt(); 70 | } 71 | if (serverSocket != null) { 72 | try { 73 | serverSocket.close(); 74 | } catch (IOException e) { 75 | e.printStackTrace(); 76 | } 77 | } 78 | } 79 | 80 | public void updatePosition(Location ownLocation) { 81 | if (clientSocket == null || clientSocket.isClosed()) { 82 | return; 83 | } 84 | // since Android 7 network requests must be async to avoid NetworkOnMainThreadException 85 | new FlarmMessageSenderTask(clientSocket, messageMap, ownLocation).execute(); 86 | } 87 | 88 | public void addFlarmMessage(FlarmMessage flarmMessage) { 89 | messageMap.put(flarmMessage.getID(), flarmMessage); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 20 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 41 | 42 | 45 | 46 | 47 | 48 | 51 | 52 | 53 | 56 | 57 | 61 | 64 | 65 | 69 | 72 | 73 | 74 | 77 | 78 | 79 | 80 | 81 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/entity/Aircraft.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common.entity; 2 | 3 | import org.ogn.commons.beacon.AircraftType; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * Created by Dominik on 04.08.2017. 9 | */ 10 | 11 | public class Aircraft { 12 | final String address; 13 | final AircraftType aircraftType; 14 | float climbRate; 15 | double lat; 16 | double lon; 17 | float alt; 18 | float groundSpeed; 19 | String regNumber; 20 | String CN; 21 | String model; 22 | boolean isOgnPrivate; 23 | String receiverName; 24 | int track; 25 | Date lastSeen; 26 | 27 | public Aircraft(String address, AircraftType aircraftType, float climbRate, double lat, double lon, 28 | float alt, float groundSpeed, String regNumber, String CN, String model, boolean isOgnPrivate, 29 | String receiverName, int track) { 30 | this.address = address; 31 | this.aircraftType = aircraftType; 32 | this.climbRate = climbRate; 33 | this.lat = lat; 34 | this.lon = lon; 35 | this.alt = alt; 36 | this.groundSpeed = groundSpeed; 37 | this.regNumber = regNumber; 38 | this.CN = CN; 39 | this.model = model; 40 | this.isOgnPrivate = isOgnPrivate; 41 | this.receiverName = receiverName; 42 | this.track = track; 43 | this.lastSeen = new Date(); 44 | } 45 | 46 | public String getAddress() { 47 | return address; 48 | } 49 | 50 | public AircraftType getAircraftType() { 51 | return aircraftType; 52 | } 53 | 54 | public float getClimbRate() { 55 | return climbRate; 56 | } 57 | 58 | public void setClimbRate(float climbRate) { 59 | this.climbRate = climbRate; 60 | } 61 | 62 | public double getLat() { 63 | return lat; 64 | } 65 | 66 | public void setLat(double lat) { 67 | this.lat = lat; 68 | } 69 | 70 | public double getLon() { 71 | return lon; 72 | } 73 | 74 | public void setLon(double lon) { 75 | this.lon = lon; 76 | } 77 | 78 | public float getAlt() { 79 | return alt; 80 | } 81 | 82 | public void setAlt(float alt) { 83 | this.alt = alt; 84 | } 85 | 86 | public float getGroundSpeed() { 87 | return groundSpeed; 88 | } 89 | 90 | public void setGroundSpeed(float groundSpeed) { 91 | this.groundSpeed = groundSpeed; 92 | } 93 | 94 | public String getRegNumber() { 95 | return regNumber; 96 | } 97 | 98 | public void setRegNumber(String regNumber) { 99 | this.regNumber = regNumber; 100 | } 101 | 102 | public String getCN() { 103 | return CN; 104 | } 105 | 106 | public void setCN(String CN) { 107 | this.CN = CN; 108 | } 109 | 110 | public String getModel() { 111 | return model; 112 | } 113 | 114 | public void setModel(String model) { 115 | this.model = model; 116 | } 117 | 118 | public boolean isOgnPrivate() { 119 | return isOgnPrivate; 120 | } 121 | 122 | public void setOgnPrivate(boolean ognPrivate) { 123 | isOgnPrivate = ognPrivate; 124 | } 125 | 126 | public String getReceiverName() { 127 | return receiverName; 128 | } 129 | 130 | public void setReceiverName(String receiverName) { 131 | this.receiverName = receiverName; 132 | } 133 | 134 | public int getTrack() { 135 | return track; 136 | } 137 | 138 | public void setTrack(int track) { 139 | this.track = track; 140 | } 141 | 142 | public Date getLastSeen() { 143 | return lastSeen; 144 | } 145 | 146 | public void setLastSeen(Date lastSeen) { 147 | this.lastSeen = lastSeen; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/ui/AircraftDialog.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.ui; 2 | 3 | 4 | import android.app.AlertDialog; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.widget.EditText; 10 | 11 | import com.meisterschueler.ognviewer.common.AircraftDescriptorProviderHelper; 12 | import com.meisterschueler.ognviewer.CustomAircraftDescriptor; 13 | import com.meisterschueler.ognviewer.common.CustomAircraftDescriptorProvider; 14 | import com.meisterschueler.ognviewer.R; 15 | 16 | import org.ogn.commons.beacon.AircraftDescriptor; 17 | import org.ogn.commons.beacon.descriptor.AircraftDescriptorProvider; 18 | 19 | public class AircraftDialog { 20 | 21 | public static void showDialog(Context context, final String address) { 22 | AircraftDescriptorProvider adp1 = AircraftDescriptorProviderHelper.getCustomDbAircraftDescriptorProvider(); 23 | AircraftDescriptorProvider adp2 = AircraftDescriptorProviderHelper.getOgnDbAircraftDescriptorProvider(); 24 | 25 | AircraftDescriptor descriptor = adp1.findDescriptor(address); 26 | if (descriptor == null) { 27 | descriptor = adp2.findDescriptor(address); 28 | } 29 | 30 | CustomAircraftDescriptor cad; 31 | if (descriptor != null) { 32 | cad = new CustomAircraftDescriptor(address, descriptor); 33 | } else { 34 | cad = new CustomAircraftDescriptor(address, "", "", "", "", "", ""); 35 | } 36 | 37 | showDialog(context, cad, null); 38 | } 39 | 40 | public static void showEmptyDialog(Context context, AircraftDialogCallback callback) { 41 | CustomAircraftDescriptor cad = new CustomAircraftDescriptor("", "", "", "", "", "", ""); 42 | showDialog(context, cad, callback); 43 | } 44 | 45 | public static void showDialog(Context context, final CustomAircraftDescriptor cad, final AircraftDialogCallback callback) { 46 | View view = LayoutInflater.from(context).inflate(R.layout.dialog_aircraft, null); 47 | 48 | final EditText etAddress = view.findViewById(R.id.editTextAddress); 49 | final EditText etRegNumber = view.findViewById(R.id.editTextRegId); 50 | final EditText etCN = view.findViewById(R.id.editTextCN); 51 | final EditText etOwner = view.findViewById(R.id.editTextOwner); 52 | final EditText etHomeBase = view.findViewById(R.id.editTextHomeBase); 53 | final EditText etModel = view.findViewById(R.id.editTextModel); 54 | final EditText etFreq = view.findViewById(R.id.editTextFreq); 55 | 56 | etAddress.setText(cad.address); 57 | etRegNumber.setText(cad.regNumber); 58 | etCN.setText(cad.CN); 59 | etOwner.setText(cad.owner); 60 | etHomeBase.setText(cad.homeBase); 61 | etModel.setText(cad.model); 62 | etFreq.setText(cad.freq); 63 | 64 | new AlertDialog.Builder(context) 65 | .setView(view) 66 | .setTitle("Edit aircraft information") 67 | .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 68 | @Override 69 | public void onClick(DialogInterface dialog, int id) { 70 | cad.address = etAddress.getText().toString(); 71 | cad.regNumber = etRegNumber.getText().toString(); 72 | cad.CN = etCN.getText().toString(); 73 | cad.owner = etOwner.getText().toString(); 74 | cad.homeBase = etHomeBase.getText().toString(); 75 | cad.model = etModel.getText().toString(); 76 | cad.freq = etFreq.getText().toString(); 77 | 78 | CustomAircraftDescriptorProvider cadp = (CustomAircraftDescriptorProvider) AircraftDescriptorProviderHelper.getCustomDbAircraftDescriptorProvider(); 79 | cadp.saveCustomAircraftDescriptor(cad); 80 | if (callback != null) { 81 | callback.notifyUpdated(); 82 | } 83 | } 84 | }) 85 | .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 86 | @Override 87 | public void onClick(DialogInterface dialog, int which) { 88 | // cancel... 89 | } 90 | }) 91 | .create() 92 | .show(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/FlarmMessage.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | import android.location.Location; 4 | 5 | import org.ogn.commons.beacon.AircraftBeacon; 6 | 7 | import java.util.Calendar; 8 | import java.util.Locale; 9 | 10 | public class FlarmMessage { 11 | private Location beaconLocation; 12 | private long time; 13 | private double bearing = 0.0; 14 | private float distance = 0.0f; 15 | 16 | private int AlarmLevel; // decimal 0-3: 0 == no alarm, 1 == 13-18s to impact, 2 == 9-12s to impact, 3 == 0-8s to impact 17 | private int RelativeNorth; // Decimal integer value. Range: from -32768 to 32767. Relative position in meters true north from own position. 18 | private int RelativeEast; // Decimal integer value. Range: from -32768 to 32767. Relative position in meters true east from own position. 19 | private int RelativeVertical; // Decimal integer value. Range: from -32768 to 32767. Relative vertical separation in meters above own position. 20 | private int IDType; // Decimal integer value. Range: from 0 to 3. 1 == official ICAO 24-bit aircraft address, 2 == stable FLARM ID (chosen by FLARM), 3 = anonymous ID 21 | private String ID; // 6-digit hexadecimal value 22 | private int Track; // Decimal integer value. Range: from 0 to 359. 23 | private int GroundSpeed; // Decimal integer value. Range: from 0 to 32767. 24 | private float ClimbRate; // Decimal fixed point number with one digit after the radix. Range: from -32.7 to 32.7. 25 | private String AcftType; // Hexadecimal value. Range: from 0 to F. 26 | 27 | 28 | public long getTime() { 29 | return time; 30 | } 31 | 32 | public double getBearing() { 33 | return bearing; 34 | } 35 | 36 | public float getDistance() { 37 | return distance; 38 | } 39 | 40 | public int getAlarmLevel() { 41 | return AlarmLevel; 42 | } 43 | 44 | public int getRelativeNorth() { 45 | return RelativeNorth; 46 | } 47 | 48 | public int getRelativeEast() { 49 | return RelativeEast; 50 | } 51 | 52 | public int getRelativeVertical() { 53 | return RelativeVertical; 54 | } 55 | 56 | public int getIDType() { 57 | return IDType; 58 | } 59 | 60 | public String getID() { 61 | return ID; 62 | } 63 | 64 | public int getTrack() { 65 | return Track; 66 | } 67 | 68 | public int getGroundSpeed() { 69 | return GroundSpeed; 70 | } 71 | 72 | public float getClimbRate() { 73 | return ClimbRate; 74 | } 75 | 76 | public String getAcftType() { 77 | return AcftType; 78 | } 79 | 80 | public FlarmMessage(AircraftBeacon aircraftBeacon) { 81 | this.time = Calendar.getInstance().getTime().getTime(); 82 | 83 | this.beaconLocation = new Location("OGN"); 84 | this.beaconLocation.setLatitude(aircraftBeacon.getLat()); 85 | this.beaconLocation.setLongitude(aircraftBeacon.getLon()); 86 | this.beaconLocation.setAltitude(aircraftBeacon.getAlt()); 87 | this.beaconLocation.setBearing(aircraftBeacon.getTrack()); 88 | this.beaconLocation.setSpeed(aircraftBeacon.getGroundSpeed()); 89 | 90 | this.AlarmLevel = 0; 91 | this.IDType = aircraftBeacon.getAddressType().getCode(); 92 | this.ID = aircraftBeacon.getAddress(); 93 | this.Track = aircraftBeacon.getTrack(); 94 | this.GroundSpeed = (int) aircraftBeacon.getGroundSpeed(); 95 | this.ClimbRate = aircraftBeacon.getClimbRate(); 96 | this.AcftType = Integer.toHexString(aircraftBeacon.getAircraftType().getCode()); 97 | } 98 | 99 | public void setOwnLocation(Location ownLocation) { 100 | this.bearing = ownLocation.bearingTo(beaconLocation) / 180.0 * Math.PI; 101 | this.distance = ownLocation.distanceTo(beaconLocation); 102 | 103 | this.RelativeNorth = ((int) Math.round(Math.cos(bearing) * distance)); 104 | this.RelativeEast = ((int) Math.round(Math.sin(bearing) * distance)); 105 | this.RelativeVertical = ((int) Math.round(beaconLocation.getAltitude() - ownLocation.getAltitude())); 106 | } 107 | 108 | static public String checksum(String str) { 109 | int result = 0; 110 | for(int i = 0; i < str.length(); i++) { 111 | result ^= str.charAt(i); 112 | } 113 | return String.format("%02X", result); 114 | } 115 | 116 | public String toString() { 117 | String result = String.format(Locale.US,"PFLAA,%d,%d,%d,%d,%d,%s,%d,,%d,%.1f,%s", AlarmLevel, RelativeNorth, RelativeEast, RelativeVertical, IDType, ID, Track, GroundSpeed, ClimbRate, AcftType); 118 | return String.format(Locale.US,"$%s*%s", result, checksum(result)); 119 | 120 | // PFLAA,,,, 121 | // ,,,,,, 122 | // , 123 | } 124 | } -------------------------------------------------------------------------------- /app/src/test/java/com/meisterschueler/ognviewer/common/FlarmMessageTest.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | import android.location.Location; 4 | 5 | import com.meisterschueler.ognviewer.common.FlarmMessage; 6 | 7 | import org.junit.Assert; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.ogn.commons.beacon.AircraftBeacon; 12 | import org.ogn.commons.beacon.impl.aprs.AprsLineParser; 13 | import org.robolectric.RobolectricTestRunner; 14 | 15 | import static org.ogn.commons.utils.AprsUtils.feetsToMetres; 16 | import static org.ogn.commons.utils.AprsUtils.kntToKmh; 17 | 18 | 19 | @RunWith(RobolectricTestRunner.class) 20 | public class FlarmMessageTest { 21 | Location location; 22 | AprsLineParser parser; 23 | 24 | @Before 25 | public void setUp() throws Exception { 26 | location = new Location("Test"); 27 | location.setAccuracy(10); 28 | location.setTime(1000); 29 | location.setLatitude(51.0d); 30 | location.setLongitude(13.0d); 31 | location.setAltitude(600); 32 | 33 | parser = AprsLineParser.get(); 34 | } 35 | 36 | // 51N 13E to 51N 14E or 51N 12E == 69.98km 37 | // 51N 13E to 52N 13E or 50N 13E == 111.2km 38 | 39 | @Test 40 | public void test_basic() { 41 | AircraftBeacon ab = (AircraftBeacon) parser.parse("ICA4B0E3A>APRS,qAS,Letzi:/165319h5100.00N\\01300.00E^327/149/A=006498 id0ADDA5BA -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5"); 42 | 43 | FlarmMessage flarmMessage = new FlarmMessage(ab); 44 | flarmMessage.setOwnLocation(location); 45 | Assert.assertEquals(flarmMessage.getID(), "DDA5BA"); 46 | Assert.assertEquals(flarmMessage.getTrack(), 327, 0.01); 47 | Assert.assertEquals(flarmMessage.getGroundSpeed(), kntToKmh(149), 1.0); 48 | Assert.assertEquals(flarmMessage.getClimbRate(), feetsToMetres(-454)/60.0, 0.01); 49 | Assert.assertEquals(flarmMessage.getAcftType(), "2"); 50 | } 51 | 52 | @Test 53 | public void test_above() { 54 | AircraftBeacon ab = (AircraftBeacon) parser.parse("ICA4B0E3A>APRS,qAS,Letzi:/165319h5100.00N\\01300.00E^327/149/A=006498 id0ADDA5BA -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5"); 55 | 56 | FlarmMessage flarmMessage = new FlarmMessage(ab); 57 | flarmMessage.setOwnLocation(location); 58 | Assert.assertEquals(flarmMessage.getRelativeEast(), 0, 0.01); 59 | Assert.assertEquals(flarmMessage.getRelativeNorth(), 0, 0.01); 60 | Assert.assertEquals(flarmMessage.getRelativeVertical(), feetsToMetres(6498) - location.getAltitude(), 1.0); 61 | } 62 | 63 | @Test 64 | public void test_east() { 65 | AircraftBeacon ab = (AircraftBeacon) parser.parse("ICA4B0E3A>APRS,qAS,Letzi:/165319h5100.00N\\01400.00E^327/149/A=006498 id0ADDA5BA -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5"); 66 | 67 | FlarmMessage flarmMessage = new FlarmMessage(ab); 68 | flarmMessage.setOwnLocation(location); 69 | Assert.assertEquals(flarmMessage.getRelativeNorth(), 0, 500); 70 | Assert.assertEquals(flarmMessage.getRelativeEast(), 69980, 500); 71 | } 72 | 73 | @Test 74 | public void test_west() { 75 | AircraftBeacon ab = (AircraftBeacon) parser.parse("ICA4B0E3A>APRS,qAS,Letzi:/165319h5100.00N\\01200.00E^327/149/A=006498 id0ADDA5BA -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5"); 76 | 77 | FlarmMessage flarmMessage = new FlarmMessage(ab); 78 | flarmMessage.setOwnLocation(location); 79 | Assert.assertEquals(flarmMessage.getRelativeNorth(), 0, 500); 80 | Assert.assertEquals(flarmMessage.getRelativeEast(), -69980, 500); 81 | } 82 | 83 | @Test 84 | public void test_north() { 85 | AircraftBeacon ab = (AircraftBeacon) parser.parse("ICA4B0E3A>APRS,qAS,Letzi:/165319h5200.00N\\01300.00E^327/149/A=006498 id0ADDA5BA -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5"); 86 | 87 | FlarmMessage flarmMessage = new FlarmMessage(ab); 88 | flarmMessage.setOwnLocation(location); 89 | Assert.assertEquals(flarmMessage.getRelativeNorth(), 111200, 500); 90 | Assert.assertEquals(flarmMessage.getRelativeEast(), 0, 500); 91 | } 92 | 93 | @Test 94 | public void test_south() { 95 | AircraftBeacon ab = (AircraftBeacon) parser.parse("ICA4B0E3A>APRS,qAS,Letzi:/165319h5000.00N\\01300.00E^327/149/A=006498 id0ADDA5BA -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5"); 96 | 97 | FlarmMessage flarmMessage = new FlarmMessage(ab); 98 | flarmMessage.setOwnLocation(location); 99 | Assert.assertEquals(flarmMessage.getRelativeNorth(), -111200, 500); 100 | Assert.assertEquals(flarmMessage.getRelativeEast(), 0, 500); 101 | } 102 | 103 | @Test 104 | public void test_checksum() { 105 | String s = "PFLAA,0,1,80,-6,2,DDC10E,95,,37,2.3,8";//*4E 106 | Assert.assertEquals("4E", FlarmMessage.checksum(s)); 107 | 108 | s = "PFLAA,0,-21,-49,-4,2,DDA659,0,,0,0.0,1";//*07 109 | Assert.assertEquals("07", FlarmMessage.checksum(s)); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/terrain 5 | @string/hybrid 6 | @string/satellite 7 | @string/map_none 8 | @string/map_normal 9 | 10 | 11 | 12 | @string/terrain 13 | @string/hybrid 14 | @string/satellite 15 | @string/map_none 16 | @string/map_normal 17 | 18 | 19 | 20 | @string/orientation_automatic 21 | @string/orientation_portrait 22 | @string/orientation_landscape_lr 23 | 24 | 25 | 26 | @string/orientation_automatic 27 | @string/orientation_portrait 28 | @string/orientation_landscape_lr 29 | 30 | 31 | 32 | @string/flightpath_none 33 | @string/flightpath_standard 34 | @string/flightpath_multicolor_descr 35 | 36 | 37 | 38 | @string/flightpath_none 39 | @string/flightpath_standard 40 | @string/flightpath_multicolor 41 | 42 | 43 | 44 | @string/altitude 45 | @string/speed 46 | @string/aircraft_type 47 | 48 | 49 | 50 | @string/altitude 51 | @string/speed 52 | @string/aircraft_type 53 | 54 | 55 | 56 | @string/time_30s 57 | @string/time_1m 58 | @string/time_2m 59 | @string/time_5m 60 | @string/time_10m 61 | @string/time_30m 62 | @string/time_1h 63 | 64 | 65 | 66 | @string/time_30s 67 | @string/time_1m 68 | @string/time_2m 69 | @string/time_5m 70 | @string/time_10m 71 | @string/time_30m 72 | @string/time_1h 73 | 74 | 75 | 76 | @string/distance_10km 77 | @string/distance_20km 78 | @string/distance_30km 79 | @string/distance_40km 80 | @string/distance_50km 81 | @string/distance_60km 82 | @string/distance_70km 83 | @string/distance_80km 84 | @string/distance_90km 85 | @string/distance_100km 86 | 87 | 88 | 89 | @string/distance_10km 90 | @string/distance_20km 91 | @string/distance_30km 92 | @string/distance_40km 93 | @string/distance_50km 94 | @string/distance_60km 95 | @string/distance_70km 96 | @string/distance_80km 97 | @string/distance_90km 98 | @string/distance_100km 99 | 100 | 101 | 102 | @string/aircraft_count 103 | @string/beacon_count 104 | @string/altitude 105 | 106 | 107 | 108 | @string/aircraft_count 109 | @string/beacon_count 110 | @string/altitude 111 | 112 | 113 | 114 | @string/unit_meters 115 | @string/unit_feet 116 | 117 | 118 | 119 | @string/unit_meters 120 | @string/unit_feet 121 | 122 | 123 | 124 | @string/unit_kmh 125 | @string/unit_mph 126 | @string/unit_kt 127 | 128 | 129 | 130 | @string/unit_kmh 131 | @string/unit_mph 132 | @string/unit_kt 133 | 134 | 135 | 136 | @string/unit_ms 137 | @string/unit_fpm 138 | 139 | 140 | 141 | @string/unit_ms 142 | @string/unit_fpm 143 | 144 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/CustomAircraftDescriptorProvider.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | import com.meisterschueler.ognviewer.CustomAircraftDescriptor; 4 | 5 | import org.apache.commons.csv.CSVFormat; 6 | import org.apache.commons.csv.CSVParser; 7 | import org.apache.commons.csv.CSVPrinter; 8 | import org.apache.commons.csv.CSVRecord; 9 | import org.ogn.commons.beacon.AircraftDescriptor; 10 | import org.ogn.commons.beacon.descriptor.AircraftDescriptorProvider; 11 | import org.ogn.commons.beacon.impl.AircraftDescriptorImpl; 12 | 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.InputStreamReader; 16 | import java.io.OutputStream; 17 | import java.io.OutputStreamWriter; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | import co.uk.rushorm.core.RushCore; 23 | import co.uk.rushorm.core.RushSearch; 24 | import timber.log.Timber; 25 | 26 | public class CustomAircraftDescriptorProvider implements AircraftDescriptorProvider { 27 | 28 | private Map aircraftDescriptorMap = new HashMap<>(); 29 | 30 | private final String CSV_ADDRESS_KEY = "Address"; 31 | private final String CSV_REGNUMBER_KEY = "RegNumber"; 32 | private final String CSV_COMPETITIONNAME_KEY = "CN"; 33 | private final String CSV_OWNER_KEY = "Owner"; 34 | private final String CSV_MODEL_KEY = "Model"; 35 | 36 | 37 | public CustomAircraftDescriptorProvider() { 38 | List cads = new RushSearch().find(CustomAircraftDescriptor.class); 39 | for (CustomAircraftDescriptor cad : cads) { 40 | aircraftDescriptorMap.put(cad.address, new CustomAircraftDescriptor(cad.address, cad.regNumber, cad.CN, cad.owner, cad.homeBase, cad.model, cad.freq)); 41 | } 42 | } 43 | 44 | public void saveCustomAircraftDescriptor(CustomAircraftDescriptor cad) { 45 | if (cad.isEmpty()) { 46 | aircraftDescriptorMap.remove(cad.address); 47 | cad = new RushSearch().whereEqual("address", cad.address).findSingle(CustomAircraftDescriptor.class); 48 | if (cad != null) { 49 | cad.delete(); 50 | } 51 | } else { 52 | aircraftDescriptorMap.put(cad.address, cad); 53 | cad.save(); 54 | } 55 | } 56 | 57 | public void removeCustomAircraftDescriptor(CustomAircraftDescriptor cad) { 58 | aircraftDescriptorMap.remove(cad.address); 59 | cad = new RushSearch().whereEqual("address", cad.address).findSingle(CustomAircraftDescriptor.class); 60 | if (cad != null) { 61 | cad.delete(); 62 | } 63 | } 64 | 65 | public void removeAll() { 66 | RushCore.getInstance().deleteAll(CustomAircraftDescriptor.class); 67 | aircraftDescriptorMap.clear(); 68 | } 69 | 70 | public int writeToFile(OutputStream outputStream) { 71 | try ( 72 | OutputStreamWriter writer = new OutputStreamWriter(outputStream); 73 | CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT 74 | .withHeader(CSV_ADDRESS_KEY, CSV_REGNUMBER_KEY, CSV_COMPETITIONNAME_KEY, CSV_OWNER_KEY, CSV_MODEL_KEY)); 75 | ) { 76 | int exportCount = 0; 77 | for (CustomAircraftDescriptor cad : aircraftDescriptorMap.values()) { 78 | csvPrinter.printRecord(cad.address, cad.regNumber, cad.CN, cad.owner, cad.model); 79 | exportCount++; 80 | } 81 | 82 | return exportCount; 83 | } catch (IOException ex) { 84 | Timber.wtf("Error while exporting aircraft to file. %s", ex.getMessage()); 85 | return -1; 86 | } 87 | } 88 | 89 | public int readFromFile(InputStream inputStream) { 90 | try ( 91 | InputStreamReader reader = new InputStreamReader(inputStream); 92 | CSVParser csvParser = new CSVParser(reader, CSVFormat.DEFAULT 93 | .withHeader(CSV_ADDRESS_KEY, CSV_REGNUMBER_KEY, CSV_COMPETITIONNAME_KEY, CSV_OWNER_KEY, CSV_MODEL_KEY) 94 | .withFirstRecordAsHeader() 95 | .withIgnoreHeaderCase() 96 | .withTrim()); 97 | ) { 98 | int importCount = 0; 99 | for (CSVRecord csvRecord : csvParser) { 100 | if (!aircraftDescriptorMap.containsKey(csvRecord.get(CSV_ADDRESS_KEY))) { 101 | CustomAircraftDescriptor cad = new CustomAircraftDescriptor(csvRecord.get(CSV_ADDRESS_KEY), 102 | csvRecord.get(CSV_REGNUMBER_KEY), csvRecord.get(CSV_COMPETITIONNAME_KEY), 103 | csvRecord.get(CSV_OWNER_KEY), "", csvRecord.get(CSV_MODEL_KEY), ""); 104 | saveCustomAircraftDescriptor(cad); 105 | importCount++; 106 | } 107 | } 108 | return importCount; 109 | } catch (IOException ex) { 110 | Timber.wtf("Error while importing aircraft from file. %s", ex.getMessage()); 111 | return -1; 112 | } 113 | } 114 | @Override 115 | public AircraftDescriptor findDescriptor(String address) { 116 | CustomAircraftDescriptor cad = aircraftDescriptorMap.get(address); 117 | if (cad != null) { 118 | return new AircraftDescriptorImpl(cad.regNumber, cad.CN, cad.owner, cad.homeBase, cad.model, cad.freq, true, true); 119 | } else { 120 | return null; 121 | } 122 | } 123 | 124 | public Map getAircraftDescriptorMap() { 125 | return aircraftDescriptorMap; 126 | } 127 | 128 | public String getAprsBudlistFilter() { 129 | String buddies = ""; 130 | for (CustomAircraftDescriptor cad : aircraftDescriptorMap.values()) { 131 | buddies += "/FLR" + cad.address + "/ICA" + cad.address + "/OGN" + cad.address + "/FNT" + cad.address; 132 | } 133 | if (buddies.isEmpty()) { 134 | return ""; 135 | } else { 136 | return "b" + buddies; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/common/ReceiverBeaconImplReplacement.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.common; 2 | 3 | import org.ogn.commons.beacon.ReceiverBeacon; 4 | import org.ogn.commons.utils.Version; 5 | 6 | /** 7 | * Created by Dominik on 07.02.2018. 8 | * For details see Code from AprsReceiverBeacon in org.ogn.commons.beacon.impl.aprs 9 | */ 10 | 11 | public class ReceiverBeaconImplReplacement implements ReceiverBeacon { 12 | 13 | 14 | /** 15 | * name of the server receiving the packet 16 | */ 17 | protected String srvName; 18 | 19 | /** 20 | * receiver's version 21 | */ 22 | protected String version; 23 | 24 | /** 25 | * hardware platform on which the receiver runs 26 | */ 27 | protected String platform; 28 | 29 | /** 30 | * CPU load (as indicated by the linux 'uptime' command) 31 | */ 32 | protected float cpuLoad; 33 | 34 | /** 35 | * CPU temperature of the board (in deg C) or Float.NaN if not 36 | * set 37 | */ 38 | protected float cpuTemp = Float.NaN; 39 | 40 | /** 41 | * total size of RAM available in the system (in MB) 42 | */ 43 | protected float totalRam; 44 | 45 | /** 46 | * size of free RAM (in MB) 47 | */ 48 | protected float freeRam; 49 | 50 | /** 51 | * estimated NTP error (in ms) 52 | */ 53 | protected float ntpError; 54 | 55 | /** 56 | * real time crystal correction(set in the configuration) (in ppm) 57 | */ 58 | protected float rtCrystalCorrection; 59 | 60 | /** 61 | * receiver (DVB-T stick's) crystal correction (in ppm) 62 | */ 63 | protected int recCrystalCorrection; 64 | 65 | /** 66 | * receiver correction measured taking GSM for a reference (in ppm) 67 | */ 68 | protected float recCrystalCorrectionFine; 69 | 70 | /** 71 | * receiver's input noise (in dB) 72 | */ 73 | protected float recInputNoise; 74 | 75 | 76 | 77 | @Override 78 | public float getCpuLoad() { 79 | return cpuLoad; 80 | } 81 | 82 | @Override 83 | public float getCpuTemp() { 84 | return cpuTemp; 85 | } 86 | 87 | @Override 88 | public float getFreeRam() { 89 | return freeRam; 90 | } 91 | 92 | @Override 93 | public float getTotalRam() { 94 | return totalRam; 95 | } 96 | 97 | @Override 98 | public float getNtpError() { 99 | return ntpError; 100 | } 101 | 102 | @Override 103 | public float getRtCrystalCorrection() { 104 | return rtCrystalCorrection; 105 | } 106 | 107 | @Override 108 | public int getRecCrystalCorrection() { 109 | return recCrystalCorrection; 110 | } 111 | 112 | @Override 113 | public float getRecCrystalCorrectionFine() { 114 | return recCrystalCorrectionFine; 115 | } 116 | 117 | @Override 118 | public float getRecAbsCorrection() { 119 | return recCrystalCorrection + recCrystalCorrectionFine; 120 | } 121 | 122 | @Override 123 | public float getRecInputNoise() { 124 | return recInputNoise; 125 | } 126 | 127 | @Override 128 | public String getServerName() { 129 | return srvName; 130 | } 131 | 132 | @Override 133 | public String getVersion() { 134 | return version; 135 | } 136 | 137 | @Override 138 | public String getPlatform() { 139 | return platform; 140 | } 141 | 142 | @Override 143 | public int getNumericVersion() { 144 | return version == null ? 0 : Version.fromString(version); 145 | } 146 | 147 | 148 | //the following things are from OgnBeacon 149 | protected String id; 150 | protected long timestamp; 151 | protected double lat; 152 | protected double lon; 153 | protected float alt; 154 | 155 | /** 156 | * deg 157 | */ 158 | protected int track; 159 | 160 | /** 161 | * km/h 162 | */ 163 | protected float groundSpeed; 164 | 165 | protected String rawPacket; 166 | 167 | @Override 168 | public String getId() { 169 | return id; 170 | } 171 | 172 | @Override 173 | public long getTimestamp() { 174 | return timestamp; 175 | } 176 | 177 | @Override 178 | public double getLat() { 179 | return lat; 180 | } 181 | 182 | @Override 183 | public double getLon() { 184 | return lon; 185 | } 186 | 187 | @Override 188 | public float getAlt() { 189 | return alt; 190 | } 191 | 192 | @Override 193 | public int getTrack() { 194 | return track; 195 | } 196 | 197 | @Override 198 | public float getGroundSpeed() { 199 | return groundSpeed; 200 | } 201 | 202 | @Override 203 | public String getRawPacket() { 204 | return rawPacket; 205 | } 206 | 207 | private ReceiverBeaconImplReplacement() { 208 | // no default implementation 209 | } 210 | 211 | public ReceiverBeaconImplReplacement(ReceiverBeacon beacon) { 212 | this.alt = beacon.getAlt(); 213 | this.cpuLoad = beacon.getCpuLoad(); 214 | this.cpuTemp = beacon.getCpuTemp(); 215 | this.cpuLoad = beacon.getCpuLoad(); 216 | this.freeRam = beacon.getFreeRam(); 217 | this.groundSpeed = beacon.getGroundSpeed(); 218 | this.id = beacon.getId(); 219 | this.lat = beacon.getLat(); 220 | this.lon = beacon.getLon(); 221 | this.ntpError = beacon.getNtpError(); 222 | this.platform = beacon.getPlatform(); 223 | this.rawPacket = beacon.getRawPacket(); 224 | this.recCrystalCorrection = beacon.getRecCrystalCorrection(); 225 | this.recCrystalCorrectionFine = beacon.getRecCrystalCorrectionFine(); 226 | this.recInputNoise = beacon.getRecInputNoise(); 227 | this.rtCrystalCorrection = beacon.getRtCrystalCorrection(); 228 | this.srvName = beacon.getServerName(); 229 | this.timestamp = beacon.getTimestamp(); 230 | this.totalRam = beacon.getTotalRam(); 231 | this.track = beacon.getTrack(); 232 | this.version = beacon.getVersion(); 233 | } 234 | 235 | public ReceiverBeaconImplReplacement update(ReceiverBeacon beacon) { 236 | if (beacon.getAlt() != 0) { 237 | this.alt = beacon.getAlt(); 238 | } 239 | if (beacon.getLat() != 0) { 240 | this.lat = beacon.getLat(); 241 | } 242 | if (beacon.getLon() != 0) { 243 | this.lon = beacon.getLon(); 244 | } 245 | this.timestamp = beacon.getTimestamp(); 246 | //add more details to update if needed 247 | 248 | return this; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/ui/ManageIDsFragment.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.ui; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.app.AlertDialog; 6 | import android.content.DialogInterface; 7 | import android.content.Intent; 8 | import android.content.pm.PackageManager; 9 | import android.os.Bundle; 10 | import android.view.Menu; 11 | import android.view.MenuInflater; 12 | import android.view.MenuItem; 13 | import android.view.View; 14 | import android.widget.AdapterView; 15 | import android.widget.ListView; 16 | import android.widget.Toast; 17 | 18 | import androidx.core.app.ActivityCompat; 19 | import androidx.core.content.ContextCompat; 20 | import androidx.fragment.app.ListFragment; 21 | 22 | import com.meisterschueler.ognviewer.CustomAircraftDescriptor; 23 | import com.meisterschueler.ognviewer.R; 24 | import com.meisterschueler.ognviewer.common.AircraftDescriptorProviderHelper; 25 | import com.meisterschueler.ognviewer.common.AppConstants; 26 | import com.meisterschueler.ognviewer.common.CustomAircraftDescriptorProvider; 27 | 28 | import java.io.FileNotFoundException; 29 | import java.io.InputStream; 30 | import java.io.OutputStream; 31 | import java.text.SimpleDateFormat; 32 | import java.util.Calendar; 33 | import java.util.Locale; 34 | import java.util.Map; 35 | 36 | public class ManageIDsFragment extends ListFragment implements AircraftDialogCallback { 37 | 38 | private CustomAircraftDescriptorProvider customAircraftDescriptorProvider; 39 | private Map aircraftDescriptorMap; 40 | private CustomAircraftDescriptorAdapter adapter; 41 | 42 | private static final int REQUEST_CODE_IMPORT = 123; 43 | private static final int REQUEST_CODE_EXPORT = 456; 44 | 45 | @Override 46 | public void onCreate(Bundle savedInstanceState) { 47 | super.onCreate(savedInstanceState); 48 | setHasOptionsMenu(true); 49 | } 50 | 51 | @Override 52 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 53 | inflater.inflate(R.menu.manageid_menu, menu); 54 | } 55 | 56 | @Override 57 | public boolean onOptionsItemSelected(MenuItem item) { 58 | final int id = item.getItemId(); 59 | 60 | switch (id) { 61 | case R.id.action_add_item: 62 | AircraftDialog.showEmptyDialog(getActivity(), this); 63 | break; 64 | 65 | case R.id.action_import_items: 66 | importItems(); 67 | break; 68 | 69 | case R.id.action_export_items: 70 | exportItems(); 71 | break; 72 | 73 | case R.id.action_delete_all_items: 74 | DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { 75 | @Override 76 | public void onClick(DialogInterface dialog, int which) { 77 | switch (which){ 78 | case DialogInterface.BUTTON_POSITIVE: 79 | customAircraftDescriptorProvider.removeAll(); 80 | resetListAdapter(); 81 | break; 82 | 83 | case DialogInterface.BUTTON_NEGATIVE: 84 | //No button clicked 85 | break; 86 | } 87 | } 88 | }; 89 | 90 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 91 | builder.setMessage("Are you sure?").setPositiveButton("Yes", dialogClickListener) 92 | .setNegativeButton("No", dialogClickListener).show(); 93 | break; 94 | } 95 | 96 | return super.onOptionsItemSelected(item); 97 | } 98 | 99 | @Override 100 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 101 | super.onActivityResult(requestCode, resultCode, data); 102 | 103 | if(requestCode == REQUEST_CODE_IMPORT && resultCode == Activity.RESULT_OK && data != null) { 104 | try { 105 | InputStream inputStream = getContext().getContentResolver().openInputStream(data.getData()); 106 | int importCount = customAircraftDescriptorProvider.readFromFile(inputStream); 107 | if (importCount > -1) { 108 | resetListAdapter(); 109 | Toast.makeText(getActivity(), "Imported " + importCount + " aircraft.", Toast.LENGTH_LONG).show(); 110 | } else { 111 | Toast.makeText(getActivity(), "Error during import.", Toast.LENGTH_LONG).show(); 112 | } 113 | } catch (FileNotFoundException e) { 114 | e.printStackTrace(); 115 | } 116 | 117 | 118 | } else if(requestCode == REQUEST_CODE_EXPORT && resultCode == Activity.RESULT_OK && data != null) { 119 | try { 120 | OutputStream outputStream = getContext().getContentResolver().openOutputStream(data.getData()); 121 | int exportCount = customAircraftDescriptorProvider.writeToFile(outputStream); 122 | if (exportCount > -1) { 123 | Toast.makeText(getActivity(), "Exported " + exportCount + " aircraft.", Toast.LENGTH_LONG).show(); 124 | } else { 125 | Toast.makeText(getActivity(), "Error during export.", Toast.LENGTH_LONG).show(); 126 | } 127 | } catch (FileNotFoundException e) { 128 | e.printStackTrace(); 129 | } 130 | 131 | 132 | } 133 | } 134 | 135 | @Override 136 | public void onActivityCreated(Bundle savedInstanceState) { 137 | super.onActivityCreated(savedInstanceState); 138 | 139 | customAircraftDescriptorProvider = (CustomAircraftDescriptorProvider) AircraftDescriptorProviderHelper.getCustomDbAircraftDescriptorProvider(); 140 | aircraftDescriptorMap = customAircraftDescriptorProvider.getAircraftDescriptorMap(); 141 | 142 | resetListAdapter(); 143 | 144 | getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { 145 | 146 | @Override 147 | public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { 148 | final CustomAircraftDescriptor cad = adapter.getItem(position); 149 | 150 | if (cad != null) { 151 | new AlertDialog.Builder(getActivity()) 152 | .setTitle("Remove " + cad.address + " ?") 153 | .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { 154 | @Override 155 | public void onClick(DialogInterface dialog, int id) { 156 | customAircraftDescriptorProvider.removeCustomAircraftDescriptor(cad); 157 | resetListAdapter(); 158 | } 159 | }) 160 | .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { 161 | @Override 162 | public void onClick(DialogInterface dialog, int which) { 163 | // cancel... 164 | } 165 | }) 166 | .create() 167 | .show(); 168 | } 169 | 170 | return true; //true to prevent call of other click listeners 171 | } 172 | }); 173 | } 174 | 175 | @Override 176 | public void onListItemClick(ListView l, View v, int position, long id) { 177 | CustomAircraftDescriptor cad = (CustomAircraftDescriptor) getListAdapter().getItem(position); 178 | AircraftDialog.showDialog(getActivity(), cad, this); 179 | } 180 | 181 | private void resetListAdapter() { 182 | adapter = new CustomAircraftDescriptorAdapter(getActivity(), aircraftDescriptorMap.values().toArray(new CustomAircraftDescriptor[aircraftDescriptorMap.size()])); 183 | setListAdapter(adapter); 184 | } 185 | 186 | @Override 187 | public void notifyUpdated() { 188 | resetListAdapter(); 189 | } 190 | 191 | private boolean checkStoragePermissions(int requestCode) { 192 | if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 193 | // Permission is not granted 194 | ActivityCompat.requestPermissions(getActivity(), 195 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 196 | requestCode); 197 | return false; 198 | } 199 | 200 | return true; 201 | 202 | } 203 | 204 | public void importItems() { 205 | if (checkStoragePermissions(AppConstants.REQUEST_CODE_STORAGE_IMPORT)) { 206 | Intent intentImport = new Intent(Intent.ACTION_GET_CONTENT); 207 | intentImport.addCategory(Intent.CATEGORY_OPENABLE); 208 | intentImport.setType("text/comma-separated-values"); 209 | startActivityForResult(intentImport, REQUEST_CODE_IMPORT); 210 | } 211 | 212 | } 213 | 214 | public void exportItems() { 215 | if (checkStoragePermissions(AppConstants.REQUEST_CODE_STORAGE_EXPORT)) { 216 | String currentTimeString = new SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.getDefault()) 217 | .format(Calendar.getInstance().getTime()); 218 | 219 | String defaultFilename = "ognviewer_export_" + currentTimeString +".csv"; 220 | 221 | Intent intentExport = new Intent(Intent.ACTION_CREATE_DOCUMENT); 222 | intentExport.addCategory(Intent.CATEGORY_OPENABLE); 223 | intentExport.setType("text/comma-separated-values"); 224 | intentExport.putExtra(Intent.EXTRA_TITLE, defaultFilename); 225 | startActivityForResult(intentExport, REQUEST_CODE_EXPORT); 226 | } 227 | 228 | } 229 | 230 | } 231 | -------------------------------------------------------------------------------- /app/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 13 | 14 | 22 | 23 | 30 | 31 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 51 | 52 | 57 | 58 | 66 | 67 | 75 | 76 | 81 | 82 | 83 | 84 | 87 | 88 | 93 | 94 | 99 | 100 | 105 | 106 | 114 | 115 | 123 | 124 | 132 | 133 | 134 | 135 | 138 | 139 | 144 | 145 | 153 | 154 | 155 | 156 | 159 | 160 | 168 | 169 | 177 | 178 | 186 | 187 | 188 | 189 | 192 | 193 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | OGN Viewer 3 | OGN Viewer 4 | 5 | 6 | (empty APRS filter --- danger!) 7 | "Set APRS filter" 8 | "Caution: An empty APRS filter can make the app extreme slow. Long click your position on the map to set a new filter." 9 | "Long click your position on the map to set a new filter." 10 | 11 | 12 | Settings 13 | Manage IDs 14 | Go to current location 15 | About 16 | Exit 17 | 18 | 19 | Add item 20 | Import items 21 | Export items 22 | Delete all items 23 | ogn_notification 24 | 25 | 26 | (no radio ID) 27 | (no registration number) 28 | (no competition ID) 29 | (no owner) 30 | (no home base) 31 | (no model) 32 | (no frequency) 33 | 34 | aprsfilter_preference 35 | Manual filter 36 | Type here your stuff 37 | APRS filter 38 | (empty) 39 | 40 | aprsserver_preference 41 | APRS server 42 | Type here your stuff 43 | APRS server 44 | (empty) 45 | 46 | 47 | movingfilter_preference 48 | Moving APRS filter 49 | Type here your stuff 50 | Moving filter is active 51 | 52 | movingfilter_range_preference 53 | Moving filter range 54 | Type here your stuff 55 | Map type 56 | 57 | showaircrafts_preference 58 | Show Aircraft 59 | on 60 | 61 | showreceivers_preference 62 | Show Receivers 63 | off 64 | 65 | map_type_aircraft_preference 66 | Map type 67 | Terrain 68 | Map type 69 | 70 | screen_orientation_preference 71 | Screen Orientation 72 | Automatic 73 | Screen Orientation 74 | 75 | keepscreenon_preference 76 | Keep Screen On 77 | off 78 | 79 | shownonmoving_preference 80 | Show Non-Moving 81 | on 82 | 83 | showregistration_preference 84 | Show Registration 85 | on 86 | 87 | rotate_aircraft_preference 88 | Rotate Aircraft 89 | off 90 | 91 | aircraft_flightpath_preference 92 | Flight Path 93 | Standard 94 | Flight Path 95 | 96 | colorisation_aircraft_preference 97 | Coloration 98 | Altitude 99 | Coloration 100 | 101 | timeout_aircraft_preference 102 | Remove inactive aircraft after 103 | Timeout 104 | Timeout 105 | 106 | colorisation_receiver_preference 107 | Aircraft Count 108 | 109 | shownotactive_preference 110 | Show Not-active 111 | on 112 | 113 | altitude_preference 114 | Altitude 115 | Altitude 116 | Altitude 117 | 118 | groundspeed_preference 119 | Ground Speed 120 | Ground Speed 121 | Ground Speed 122 | 123 | verticalspeed_preference 124 | Vertical Speed 125 | Vertical Speed 126 | Vertical Speed 127 | 128 | tcp_server_preference 129 | TCP Server 130 | TCP Server 131 | TCP Server 132 | 133 | 134 | aprs.glidernet.org 135 | 136 | Terrain 137 | Hybrid 138 | Satellite 139 | None 140 | Normal 141 | 142 | Automatic 143 | Portrait 144 | Landscape L+R 145 | 146 | Altitude 147 | Speed 148 | Aircraft type 149 | 150 | 30 s 151 | 1 min 152 | 2 min 153 | 5 min 154 | 10 min 155 | 30 min 156 | 1 h 157 | 158 | 10km 159 | 20km 160 | 30km 161 | 40km 162 | 50km 163 | 60km 164 | 70km 165 | 80km 166 | 90km 167 | 100km 168 | 169 | m 170 | ft 171 | km/h 172 | mph 173 | kt 174 | m/s 175 | ft/min 176 | 177 | None 178 | Standard 179 | Multicolor 180 | Multicolor (slower) 181 | 182 | Aircraft Count 183 | Beacon Count 184 | 185 | 186 | latitude_preference 187 | longitude_preference 188 | zoom_preference 189 | 190 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/ui/PrefsFragment.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.ui; 2 | 3 | import android.Manifest; 4 | import android.content.Intent; 5 | import android.content.SharedPreferences; 6 | import android.content.pm.PackageManager; 7 | import android.os.Bundle; 8 | 9 | import androidx.core.app.ActivityCompat; 10 | import androidx.core.content.ContextCompat; 11 | import androidx.preference.Preference; 12 | import androidx.preference.PreferenceFragmentCompat; 13 | import androidx.preference.SwitchPreference; 14 | 15 | import com.meisterschueler.ognviewer.R; 16 | import com.meisterschueler.ognviewer.common.AppConstants; 17 | import com.meisterschueler.ognviewer.service.OgnService; 18 | 19 | import timber.log.Timber; 20 | 21 | public class PrefsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { 22 | 23 | @Override 24 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 25 | addPreferencesFromResource(R.xml.preferences); 26 | 27 | SharedPreferences sharedPreferences = getPreferenceScreen().getSharedPreferences(); 28 | 29 | updateFragmentValues(sharedPreferences, getString(R.string.key_movingfilter_preference)); 30 | updateFragmentValues(sharedPreferences, getString(R.string.key_movingfilter_range_preference)); 31 | updateFragmentValues(sharedPreferences, getString(R.string.key_aprsfilter_preference)); 32 | updateFragmentValues(sharedPreferences, getString(R.string.key_aprsserver_preference)); 33 | updateFragmentValues(sharedPreferences, getString(R.string.key_showaircrafts_preference)); 34 | updateFragmentValues(sharedPreferences, getString(R.string.key_showreceivers_preference)); 35 | updateFragmentValues(sharedPreferences, getString(R.string.key_map_type_preference)); 36 | updateFragmentValues(sharedPreferences, getString(R.string.key_screen_orientation_preference)); 37 | updateFragmentValues(sharedPreferences, getString(R.string.key_keepscreenon_preference)); 38 | updateFragmentValues(sharedPreferences, getString(R.string.key_shownonmoving_preference)); 39 | updateFragmentValues(sharedPreferences, getString(R.string.key_showregistration_preference)); 40 | updateFragmentValues(sharedPreferences, getString(R.string.key_rotate_aircraft_preference)); 41 | updateFragmentValues(sharedPreferences, getString(R.string.key_aircraft_flightpath_preference)); 42 | updateFragmentValues(sharedPreferences, getString(R.string.key_aircraft_colorisation_preference)); 43 | updateFragmentValues(sharedPreferences, getString(R.string.key_aircraft_timeout_preference)); 44 | updateFragmentValues(sharedPreferences, getString(R.string.key_shownotactive_preference)); 45 | updateFragmentValues(sharedPreferences, getString(R.string.key_receiver_colorisation_preference)); 46 | updateFragmentValues(sharedPreferences, getString(R.string.key_altitude_unit_preference)); 47 | updateFragmentValues(sharedPreferences, getString(R.string.key_groundspeed_unit_preference)); 48 | updateFragmentValues(sharedPreferences, getString(R.string.key_verticalspeed_unit_preference)); 49 | updateFragmentValues(sharedPreferences, getString(R.string.key_tcp_server_active_preference)); 50 | } 51 | 52 | @Override 53 | public void onResume() { 54 | super.onResume(); 55 | getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); 56 | } 57 | 58 | @Override 59 | public void onPause() { 60 | getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); 61 | super.onPause(); 62 | } 63 | 64 | @Override 65 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 66 | updateFragmentValues(sharedPreferences, key); 67 | if (key.equals(getString(R.string.key_aprsfilter_preference)) 68 | | key.equals(getString(R.string.key_aprsserver_preference)) 69 | | key.equals(getString(R.string.key_movingfilter_preference)) 70 | | key.equals(getString(R.string.key_movingfilter_range_preference))) { 71 | getActivity().startService(new Intent(getActivity(), OgnService.class)); 72 | } else if (key.equals(getString(R.string.key_tcp_server_active_preference))) { 73 | Boolean tcpServerActive = sharedPreferences.getBoolean(getString(R.string.key_tcp_server_active_preference), false); 74 | Boolean movingFilterActive = sharedPreferences.getBoolean(getString(R.string.key_movingfilter_preference), true); 75 | if (tcpServerActive | movingFilterActive) { 76 | requestLocationPermission(); 77 | } else { 78 | // TCP updates will be stopped when switched back to MapsActivity 79 | } 80 | } 81 | } 82 | 83 | private void updateFragmentValues(SharedPreferences sharedPreferences, String key) { 84 | Preference pref = findPreference(key); 85 | 86 | if (key.equals(getString(R.string.key_movingfilter_preference))) { 87 | Boolean value = sharedPreferences.getBoolean(getString(R.string.key_movingfilter_preference), true); 88 | if (value) { 89 | pref.setSummary("on"); 90 | findPreference(getString(R.string.key_movingfilter_range_preference)).setEnabled(true); 91 | findPreference(getString(R.string.key_aprsfilter_preference)).setEnabled(false); 92 | } else { 93 | pref.setSummary("off"); 94 | findPreference(getString(R.string.key_movingfilter_range_preference)).setEnabled(false); 95 | findPreference(getString(R.string.key_aprsfilter_preference)).setEnabled(true); 96 | } 97 | } else if (key.equals(getString(R.string.key_movingfilter_range_preference))) { 98 | String value = sharedPreferences.getString(getString(R.string.key_movingfilter_range_preference), getString(R.string.distance_10km)); 99 | pref.setSummary(value); 100 | } else if (key.equals(getString(R.string.key_aprsfilter_preference))) { 101 | String value = sharedPreferences.getString(getString(R.string.key_aprsfilter_preference), ""); 102 | if (value.isEmpty()) { 103 | pref.setSummary(getString(R.string.empty_aprsfilter_preference)); 104 | } else { 105 | pref.setSummary(value); 106 | } 107 | } else if (key.equals(getString(R.string.key_aprsserver_preference))) { 108 | String value = sharedPreferences.getString(getString(R.string.key_aprsserver_preference), getString(R.string.default_aprsserver)); 109 | if (value.isEmpty()) { 110 | pref.setSummary(getString(R.string.default_aprsserver)); 111 | } else { 112 | pref.setSummary(value); 113 | } 114 | 115 | } else if (key.equals(getString(R.string.key_showaircrafts_preference))) { 116 | Boolean value = sharedPreferences.getBoolean(getString(R.string.key_showaircrafts_preference), true); 117 | if (value) { 118 | pref.setSummary("on"); 119 | } else { 120 | pref.setSummary("off"); 121 | } 122 | } else if (key.equals(getString(R.string.key_showreceivers_preference))) { 123 | Boolean value = sharedPreferences.getBoolean(getString(R.string.key_showreceivers_preference), false); 124 | if (value) { 125 | pref.setSummary("on"); 126 | } else { 127 | pref.setSummary("off"); 128 | } 129 | 130 | } else if (key.equals(getString(R.string.key_map_type_preference))) { 131 | String value = sharedPreferences.getString(getString(R.string.key_map_type_preference), getString(R.string.terrain)); 132 | pref.setSummary(value); 133 | } else if (key.equals(getString(R.string.key_screen_orientation_preference))) { 134 | String value = sharedPreferences.getString(getString(R.string.key_screen_orientation_preference), getString(R.string.orientation_automatic)); 135 | pref.setSummary(value); 136 | } else if (key.equals(getString(R.string.key_keepscreenon_preference))) { 137 | Boolean value = sharedPreferences.getBoolean(getString(R.string.key_keepscreenon_preference), false); 138 | if (value) { 139 | pref.setSummary("on"); 140 | } else { 141 | pref.setSummary("off"); 142 | } 143 | } else if (key.equals(getString(R.string.key_shownonmoving_preference))) { 144 | Boolean value = sharedPreferences.getBoolean(getString(R.string.key_shownonmoving_preference), true); 145 | if (value) { 146 | pref.setSummary("on"); 147 | } else { 148 | pref.setSummary("off"); 149 | } 150 | } else if (key.equals(getString(R.string.key_showregistration_preference))) { 151 | Boolean value = sharedPreferences.getBoolean(getString(R.string.key_showregistration_preference), true); 152 | if (value) { 153 | pref.setSummary("on"); 154 | } else { 155 | pref.setSummary("off"); 156 | } 157 | } else if (key.equals(getString(R.string.key_rotate_aircraft_preference))) { 158 | Boolean value = sharedPreferences.getBoolean(getString(R.string.key_rotate_aircraft_preference), false); 159 | if (value) { 160 | pref.setSummary("on"); 161 | } else { 162 | pref.setSummary("off"); 163 | } 164 | } else if (key.equals(getString(R.string.key_aircraft_flightpath_preference))) { 165 | String value = sharedPreferences.getString(getString(R.string.key_aircraft_flightpath_preference), getString(R.string.flightpath_standard)); 166 | if (value.equals(getString(R.string.flightpath_multicolor))){ 167 | pref.setSummary(R.string.flightpath_multicolor_descr); 168 | } else { 169 | pref.setSummary(value); 170 | } 171 | } else if (key.equals(getString(R.string.key_shownotactive_preference))) { 172 | Boolean value = sharedPreferences.getBoolean(getString(R.string.key_shownotactive_preference), true); 173 | if (value) { 174 | pref.setSummary("on"); 175 | } else { 176 | pref.setSummary("off"); 177 | } 178 | } else if (key.equals(getString(R.string.key_aircraft_colorisation_preference))) { 179 | String value = sharedPreferences.getString(getString(R.string.key_aircraft_colorisation_preference), getString(R.string.altitude)); 180 | pref.setSummary(value); 181 | } else if (key.equals(getString(R.string.key_aircraft_timeout_preference))) { 182 | String value = sharedPreferences.getString(getString(R.string.key_aircraft_timeout_preference), getString(R.string.time_5m)); 183 | pref.setSummary(value); 184 | } else if (key.equals(getString(R.string.key_receiver_colorisation_preference))) { 185 | String value = sharedPreferences.getString(getString(R.string.key_receiver_colorisation_preference), getString(R.string.aircraft_count)); 186 | pref.setSummary(value); 187 | } else if (key.equals(getString(R.string.key_altitude_unit_preference))) { 188 | String value = sharedPreferences.getString(getString(R.string.key_altitude_unit_preference), getString(R.string.unit_meters)); 189 | pref.setSummary(value); 190 | } else if (key.equals(getString(R.string.key_groundspeed_unit_preference))) { 191 | String value = sharedPreferences.getString(getString(R.string.key_groundspeed_unit_preference), getString(R.string.unit_kmh)); 192 | pref.setSummary(value); 193 | } else if (key.equals(getString(R.string.key_verticalspeed_unit_preference))) { 194 | String value = sharedPreferences.getString(getString(R.string.key_verticalspeed_unit_preference), getString(R.string.unit_ms)); 195 | pref.setSummary(value); 196 | } else if (key.equals(getString(R.string.key_tcp_server_active_preference))) { 197 | Boolean value = sharedPreferences.getBoolean(getString(R.string.key_tcp_server_active_preference), false); 198 | if (value) { 199 | pref.setSummary("on"); 200 | } else { 201 | pref.setSummary("off"); 202 | } 203 | } 204 | } 205 | 206 | private void requestLocationPermission() { 207 | final String fineLocationPermissionString = Manifest.permission.ACCESS_FINE_LOCATION; 208 | 209 | if (ContextCompat.checkSelfPermission(getActivity(), fineLocationPermissionString) != PackageManager.PERMISSION_GRANTED) { 210 | ActivityCompat.requestPermissions(getActivity(), new String[]{fineLocationPermissionString}, AppConstants.REQUEST_CODE_LOCATION_TCP_UPDATES); 211 | } else { 212 | // Permission has already been granted 213 | Timber.d("Location permission granted"); 214 | } 215 | } 216 | 217 | public void setTCPServerActiveState(Boolean active) { 218 | SwitchPreference tcpPref = findPreference(getString(R.string.key_tcp_server_active_preference)); 219 | 220 | if (active) { 221 | tcpPref.setSummary("on"); 222 | tcpPref.setChecked(true); 223 | } else { 224 | tcpPref.setSummary("off"); 225 | tcpPref.setChecked(false); 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /app/src/main/java/com/meisterschueler/ognviewer/service/OgnService.java: -------------------------------------------------------------------------------- 1 | package com.meisterschueler.ognviewer.service; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.app.Notification; 6 | import android.app.NotificationChannel; 7 | import android.app.NotificationManager; 8 | import android.app.PendingIntent; 9 | import android.app.Service; 10 | import android.content.Context; 11 | import android.content.Intent; 12 | import android.content.SharedPreferences; 13 | import android.content.pm.PackageManager; 14 | import android.location.Location; 15 | import android.os.Binder; 16 | import android.os.Build; 17 | import android.os.IBinder; 18 | import android.os.Looper; 19 | import android.widget.Toast; 20 | 21 | import androidx.annotation.RequiresApi; 22 | import androidx.core.app.ActivityCompat; 23 | import androidx.core.app.NotificationCompat; 24 | import androidx.core.content.ContextCompat; 25 | import androidx.localbroadcastmanager.content.LocalBroadcastManager; 26 | import androidx.preference.PreferenceManager; 27 | 28 | import com.google.android.gms.location.FusedLocationProviderClient; 29 | import com.google.android.gms.location.LocationCallback; 30 | import com.google.android.gms.location.LocationRequest; 31 | import com.google.android.gms.location.LocationResult; 32 | import com.google.android.gms.location.LocationServices; 33 | import com.google.android.gms.location.LocationSettingsRequest; 34 | import com.google.android.gms.location.SettingsClient; 35 | import com.google.android.gms.maps.model.LatLng; 36 | import com.google.android.gms.maps.model.LatLngBounds; 37 | import com.meisterschueler.ognviewer.CustomAircraftDescriptor; 38 | import com.meisterschueler.ognviewer.R; 39 | import com.meisterschueler.ognviewer.activity.ClosingActivity; 40 | import com.meisterschueler.ognviewer.activity.MapsActivity; 41 | import com.meisterschueler.ognviewer.common.AircraftDescriptorProviderHelper; 42 | import com.meisterschueler.ognviewer.common.AppConstants; 43 | import com.meisterschueler.ognviewer.common.AprsFilterManager; 44 | import com.meisterschueler.ognviewer.common.CustomAircraftDescriptorProvider; 45 | import com.meisterschueler.ognviewer.common.FlarmMessage; 46 | import com.meisterschueler.ognviewer.common.ReceiverBeaconImplReplacement; 47 | import com.meisterschueler.ognviewer.common.ReceiverBundle; 48 | import com.meisterschueler.ognviewer.common.entity.Aircraft; 49 | import com.meisterschueler.ognviewer.common.entity.AircraftBundle; 50 | 51 | import org.ogn.client.AircraftBeaconListener; 52 | import org.ogn.client.OgnClient; 53 | import org.ogn.client.ReceiverBeaconListener; 54 | import org.ogn.commons.beacon.AircraftBeacon; 55 | import org.ogn.commons.beacon.AircraftDescriptor; 56 | import org.ogn.commons.beacon.AircraftType; 57 | import org.ogn.commons.beacon.ReceiverBeacon; 58 | 59 | import java.util.ArrayList; 60 | import java.util.Calendar; 61 | import java.util.Date; 62 | import java.util.HashMap; 63 | import java.util.Iterator; 64 | import java.util.List; 65 | import java.util.Map; 66 | import java.util.concurrent.ConcurrentHashMap; 67 | import java.util.concurrent.ScheduledExecutorService; 68 | import java.util.concurrent.ScheduledThreadPoolExecutor; 69 | import java.util.concurrent.TimeUnit; 70 | 71 | import co.uk.rushorm.android.AndroidInitializeConfig; 72 | import co.uk.rushorm.core.Rush; 73 | import co.uk.rushorm.core.RushCore; 74 | import timber.log.Timber; 75 | 76 | public class OgnService extends Service implements AircraftBeaconListener, ReceiverBeaconListener { 77 | 78 | private TcpServer tcpServer; 79 | 80 | private OgnClient ognClient; //initialized in onStartCommand 81 | private boolean ognConnected = false; //is true, when service was started (onStartCommand) 82 | private LocalBroadcastManager localBroadcastManager; 83 | private IBinder binder = new LocalBinder(); 84 | //Map aircraftMap = new ConcurrentHashMap<>(); // for WIP 2017-11-02 85 | private Map aircraftMap = new HashMap<>(); 86 | 87 | private int maxAircraftCounter = 0; 88 | private int maxBeaconCounter = 0; 89 | private Map receiverBundleMap = new ConcurrentHashMap<>(); 90 | private Map aircraftBundleMap = new ConcurrentHashMap<>(); 91 | 92 | private ScheduledExecutorService scheduledTaskExecutor; 93 | private boolean refreshingActive = false; // if true, markers on map should be updated 94 | private boolean mapCurrentlyUpdating = false; // if true, the map is currently updating (new updates should wait) 95 | private boolean timerCurrentlyRunning = false; 96 | private LatLngBounds latLngBounds = new LatLngBounds(new LatLng(0, 0), new LatLng(0, 0)); 97 | private int aircraftTimeoutInSec = AppConstants.DEFAULT_AIRCRAFT_TIMEOUT_IN_SEC; 98 | private boolean locationUpdatesAlreadyRequested = false; 99 | private FusedLocationProviderClient fusedLocationProviderClient; 100 | private LocationCallback locationCallback; 101 | 102 | private CustomAircraftDescriptorProvider customAircraftDescriptorProvider; 103 | private Location currentLocation = null; 104 | private Location movingFilterLocation = null; 105 | 106 | public Map getReceiverBundleMap() { 107 | return receiverBundleMap; 108 | } 109 | 110 | public Map getAircraftBundleMap() { 111 | return aircraftBundleMap; 112 | } 113 | 114 | public void setMapUpdatingStatus(boolean updating) { 115 | mapCurrentlyUpdating = updating; 116 | } 117 | 118 | public void setAircraftTimeout(int timoutInSec) { 119 | aircraftTimeoutInSec = timoutInSec; 120 | } 121 | 122 | public interface UpdateListener { // for WIP 2017-11-02 123 | public void updateAircraftBundle(AircraftBundle bundle); 124 | } 125 | 126 | public OgnService() { 127 | tcpServer = new TcpServer(); 128 | tcpServer.startServer(); 129 | } 130 | 131 | // Trigger new location updates at interval 132 | public void startLocationUpdates(Activity activity) { 133 | if (locationUpdatesAlreadyRequested) { 134 | return; 135 | } 136 | Context context = getApplicationContext(); 137 | long UPDATE_INTERVAL = 5 * 1000; /* 5 secs */ 138 | long FASTEST_INTERVAL = 1000; /* 1 sec */ 139 | 140 | LocationRequest locationRequest; 141 | // Create the location request to start receiving updates 142 | locationRequest = new LocationRequest(); 143 | locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); 144 | locationRequest.setInterval(UPDATE_INTERVAL); 145 | locationRequest.setFastestInterval(FASTEST_INTERVAL); 146 | 147 | // Create LocationSettingsRequest object using location request 148 | LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder(); 149 | builder.addLocationRequest(locationRequest); 150 | LocationSettingsRequest locationSettingsRequest = builder.build(); 151 | 152 | // Check whether location settings are satisfied 153 | // https://developers.google.com/android/reference/com/google/android/gms/location/SettingsClient 154 | SettingsClient settingsClient = LocationServices.getSettingsClient(context); 155 | settingsClient.checkLocationSettings(locationSettingsRequest); 156 | 157 | final String fineLocationPermissionString = Manifest.permission.ACCESS_FINE_LOCATION; 158 | final String coarseLocationPermissionString = Manifest.permission.ACCESS_COARSE_LOCATION; 159 | // maybe fine should be enough? 2018-03-12 160 | if (ContextCompat.checkSelfPermission(context, fineLocationPermissionString) != PackageManager.PERMISSION_GRANTED || 161 | ContextCompat.checkSelfPermission(context, coarseLocationPermissionString) != PackageManager.PERMISSION_GRANTED) { 162 | // Permission is not granted 163 | // Should we show an explanation? 164 | if (ActivityCompat.shouldShowRequestPermissionRationale(activity, fineLocationPermissionString)) { 165 | 166 | // Show an explanation to the user *asynchronously* -- don't block 167 | // this thread waiting for the user's response! After the user 168 | // sees the explanation, try again to request the permission. 169 | Timber.d("Location permission already denied"); 170 | // ask again? in activity? 2018-03-23 171 | // onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) 172 | // https://developer.android.com/training/permissions/requesting.html#java 173 | } else { 174 | // User was never asked to allow location updates. Ask now! 175 | ActivityCompat.requestPermissions(activity, new String[]{fineLocationPermissionString, coarseLocationPermissionString}, 176 | AppConstants.REQUEST_CODE_LOCATION_TCP_UPDATES_FROM_SERVICE); 177 | } 178 | return; 179 | } else { 180 | // Permission has already been granted 181 | Timber.d("Location permission granted"); 182 | } 183 | 184 | fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context); 185 | if (locationCallback == null) { 186 | locationCallback = new LocationCallback() { 187 | @Override 188 | public void onLocationResult(LocationResult locationResult) { 189 | currentLocation = locationResult.getLastLocation(); 190 | tcpServer.updatePosition(currentLocation); 191 | sendLocationToMap(currentLocation); 192 | if (movingFilterLocation == null || movingFilterLocation.distanceTo(currentLocation) > 5000) { 193 | movingFilterLocation = currentLocation; 194 | restartAprsClient(); 195 | } 196 | } 197 | }; 198 | } 199 | fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper()); 200 | locationUpdatesAlreadyRequested = true; 201 | } 202 | 203 | public void stopLocationUpdates() { 204 | // Does not really stop the TCP server but stop location updates! 205 | // TCP server does not send packets without location updates. 206 | if (fusedLocationProviderClient != null && locationCallback != null) { 207 | fusedLocationProviderClient.removeLocationUpdates(locationCallback); 208 | } 209 | locationUpdatesAlreadyRequested = false; 210 | } 211 | 212 | 213 | @Override 214 | public void onUpdate(AircraftBeacon aircraftBeacon, AircraftDescriptor aircraftDescriptor) { 215 | String address = aircraftBeacon.getAddress(); 216 | if (aircraftBundleMap.containsKey(address)) { 217 | long lastTimestamp = aircraftBundleMap.get(address).aircraftBeacon.getTimestamp(); 218 | long diffTimeInMS = aircraftBeacon.getTimestamp() - lastTimestamp; 219 | if (diffTimeInMS <= AppConstants.MINIMAL_AIRCRAFT_DIFF_TIME_IN_MS) { 220 | Timber.v("skipped position for %s", address); 221 | return; // skip deprecated positions 222 | } 223 | } 224 | 225 | AircraftBundle bundle = new AircraftBundle(aircraftBeacon, aircraftDescriptor); 226 | aircraftBundleMap.put(aircraftBeacon.getAddress(), bundle); 227 | 228 | ReceiverBundle receiverBundle = receiverBundleMap.get(aircraftBeacon.getReceiverName()); 229 | if (receiverBundle != null) { 230 | if (!receiverBundle.aircrafts.contains(aircraftBeacon.getId())) { 231 | receiverBundle.aircrafts.add(aircraftBeacon.getId()); 232 | } 233 | receiverBundle.beaconCount++; 234 | 235 | maxAircraftCounter = Math.max(maxAircraftCounter, receiverBundle.aircrafts.size()); 236 | maxBeaconCounter = Math.max(maxBeaconCounter, receiverBundle.beaconCount); 237 | } 238 | if (refreshingActive) { 239 | if (!sendAircraftToMap(bundle)) { 240 | Timber.d("Lost beacon for aircraft: " + aircraftBeacon.getAddress() + " " + new Date().getTime()); 241 | } 242 | } 243 | 244 | tcpServer.addFlarmMessage(new FlarmMessage(aircraftBeacon)); 245 | 246 | //for debugging 247 | Calendar c = Calendar.getInstance(); 248 | int hours = c.get(Calendar.HOUR_OF_DAY); 249 | int seconds = c.get(Calendar.SECOND); 250 | int minutes = c.get(Calendar.MINUTE); 251 | Timber.v(aircraftBundleMap.size() + " AircraftBeacons " + hours + ":" + minutes + ":" + seconds); 252 | Timber.v("Last aircraft: %s", aircraftBeacon.getAddress()); 253 | } 254 | 255 | private boolean sendAircraftToMap(AircraftBundle aircraftBundle) { 256 | AircraftBeacon aircraftBeacon = aircraftBundle.aircraftBeacon; 257 | AircraftDescriptor aircraftDescriptor = aircraftBundle.aircraftDescriptor; 258 | Intent intent = new Intent(AppConstants.INTENT_AIRCRAFT_BEACON); 259 | 260 | // AircraftBeacon 261 | intent.putExtra("receiverName", aircraftBeacon.getReceiverName()); 262 | intent.putExtra("addressType", aircraftBeacon.getAddressType().getCode()); 263 | intent.putExtra("address", aircraftBeacon.getAddress()); 264 | intent.putExtra("aircraftType", aircraftBeacon.getAircraftType().getCode()); 265 | intent.putExtra("stealth", aircraftBeacon.isStealth()); 266 | intent.putExtra("climbRate", aircraftBeacon.getClimbRate()); 267 | intent.putExtra("turnRate", aircraftBeacon.getTurnRate()); 268 | intent.putExtra("signalStrength", aircraftBeacon.getSignalStrength()); 269 | intent.putExtra("frequencyOffset", aircraftBeacon.getFrequencyOffset()); 270 | intent.putExtra("gpsStatus", aircraftBeacon.getGpsStatus()); 271 | intent.putExtra("errorCount", aircraftBeacon.getErrorCount()); 272 | //String[] getHeardAircraftIds(); 273 | 274 | // OgnBeacon 275 | intent.putExtra("id", aircraftBeacon.getId()); 276 | intent.putExtra("timestamp", aircraftBeacon.getTimestamp()); 277 | intent.putExtra("lat", aircraftBeacon.getLat()); 278 | intent.putExtra("lon", aircraftBeacon.getLon()); 279 | intent.putExtra("alt", aircraftBeacon.getAlt()); 280 | intent.putExtra("track", aircraftBeacon.getTrack()); 281 | intent.putExtra("groundSpeed", aircraftBeacon.getGroundSpeed()); 282 | intent.putExtra("rawPacket", aircraftBeacon.getRawPacket()); 283 | 284 | // AircraftDescriptor 285 | if (aircraftDescriptor != null) { 286 | intent.putExtra("known", aircraftDescriptor.isKnown()); 287 | intent.putExtra("regNumber", aircraftDescriptor.getRegNumber()); 288 | intent.putExtra("CN", aircraftDescriptor.getCN()); 289 | intent.putExtra("owner", aircraftDescriptor.getOwner()); 290 | intent.putExtra("homeBase", aircraftDescriptor.getHomeBase()); 291 | intent.putExtra("model", aircraftDescriptor.getModel()); 292 | intent.putExtra("freq", aircraftDescriptor.getFreq()); 293 | intent.putExtra("tracked", aircraftDescriptor.isTracked()); 294 | intent.putExtra("identified", aircraftDescriptor.isIdentified()); 295 | } 296 | 297 | if (!mapCurrentlyUpdating) { //check if something is updating the map currently 298 | localBroadcastManager.sendBroadcast(intent); 299 | return true; 300 | } else { 301 | return false; 302 | } 303 | } 304 | 305 | private boolean sendLocationToMap(Location location) { 306 | Intent intent = new Intent(AppConstants.INTENT_LOCATION); 307 | 308 | intent.putExtra("lat", location.getLatitude()); 309 | intent.putExtra("lon", location.getLongitude()); 310 | 311 | if (!mapCurrentlyUpdating) { //check if something is updating the map currently 312 | localBroadcastManager.sendBroadcast(intent); 313 | return true; 314 | } else { 315 | return false; 316 | } 317 | } 318 | 319 | @Override 320 | public void onUpdate(ReceiverBeacon receiverBeacon) { 321 | //CAUTION: onUpdate(ReceiverBeacon) is called two times 322 | //the first time e.g. cpuLoad, ram, platform are 0 or empty 323 | //the second time lat, long and alt are 0 324 | ReceiverBundle existingBundle = receiverBundleMap.get(receiverBeacon.getId()); 325 | ReceiverBundle bundle = new ReceiverBundle(receiverBeacon); 326 | if (existingBundle != null) { 327 | bundle.aircrafts = existingBundle.aircrafts; 328 | bundle.beaconCount = existingBundle.beaconCount; 329 | ReceiverBeaconImplReplacement beacon = new ReceiverBeaconImplReplacement(existingBundle.receiverBeacon); 330 | bundle.receiverBeacon = beacon.update(receiverBeacon); 331 | } 332 | 333 | receiverBundleMap.put(receiverBeacon.getId(), bundle); 334 | 335 | Intent intent = new Intent(AppConstants.INTENT_RECEIVER_BEACON); 336 | 337 | // ReceiverBeacon 338 | //intent.putExtra("cpuLoad", receiverBeacon.getCpuLoad()); 339 | //intent.putExtra("cpuTemp", receiverBeacon.getCpuTemp()); 340 | //intent.putExtra("freeRam", receiverBeacon.getFreeRam()); 341 | //intent.putExtra("totalRam", receiverBeacon.getTotalRam()); 342 | //intent.putExtra("ntpError", receiverBeacon.getNtpError()); 343 | //intent.putExtra("rtCrystalCorrection", receiverBeacon.getRtCrystalCorrection()); 344 | //intent.putExtra("recCrystalCorrection", receiverBeacon.getRecCrystalCorrection()); 345 | //intent.putExtra("recCrystalCorrectionFine", receiverBeacon.getRecCrystalCorrectionFine()); 346 | //intent.putExtra("recAbsCorrection", receiverBeacon.getRecAbsCorrection()); 347 | intent.putExtra("recInputNoise", receiverBeacon.getRecInputNoise()); 348 | //intent.putExtra("serverName", receiverBeacon.getServerName()); 349 | intent.putExtra("version", receiverBeacon.getVersion()); 350 | intent.putExtra("platform", receiverBeacon.getPlatform()); 351 | intent.putExtra("numericVersion", receiverBeacon.getNumericVersion()); 352 | 353 | // OgnBeacon 354 | intent.putExtra("id", receiverBeacon.getId()); 355 | intent.putExtra("timestamp", receiverBeacon.getTimestamp()); //e.g. 1517918400000L 356 | intent.putExtra("lat", receiverBeacon.getLat()); 357 | intent.putExtra("lon", receiverBeacon.getLon()); 358 | intent.putExtra("alt", receiverBeacon.getAlt()); 359 | //intent.putExtra("track", receiverBeacon.getTrack()); 360 | //intent.putExtra("groundSpeed", receiverBeacon.getGroundSpeed()); 361 | //intent.putExtra("rawPacket", receiverBeacon.getRawPacket()); 362 | 363 | // Computed Values 364 | // does this two lines make any sense here? 2018-02-06 365 | // It's already set in onUpdate(Aircraft...) 366 | maxAircraftCounter = Math.max(maxAircraftCounter, bundle.aircrafts.size()); 367 | maxBeaconCounter = Math.max(maxBeaconCounter, bundle.beaconCount); 368 | 369 | //maybe move to onUpdate(Aircraft...)? 2018-02-11 370 | ReceiverBundle.maxAircraftCounter = maxAircraftCounter; 371 | ReceiverBundle.maxBeaconCounter = maxBeaconCounter; 372 | 373 | intent.putExtra("aircraftCounter", bundle.aircrafts.size()); 374 | intent.putExtra("maxAircraftCounter", maxAircraftCounter); 375 | intent.putExtra("beaconCounter", bundle.beaconCount); 376 | intent.putExtra("maxBeaconCounter", maxBeaconCounter); 377 | 378 | if (refreshingActive) { 379 | localBroadcastManager.sendBroadcast(intent); 380 | } 381 | 382 | //for debugging 383 | Calendar c = Calendar.getInstance(); 384 | int hours = c.get(Calendar.HOUR_OF_DAY); 385 | int seconds = c.get(Calendar.SECOND); 386 | int minutes = c.get(Calendar.MINUTE); 387 | Timber.v(receiverBundleMap.size() + " ReceiverBeacons " + hours + ":" + minutes + ":" + seconds); 388 | Timber.v("Last receiver: %s", receiverBeacon.getId()); 389 | } 390 | 391 | @Override 392 | public void onCreate() { 393 | super.onCreate(); 394 | 395 | List> classes = new ArrayList<>(); 396 | classes.add(CustomAircraftDescriptor.class); 397 | AndroidInitializeConfig config = new AndroidInitializeConfig(getApplicationContext()); 398 | config.setClasses(classes); 399 | RushCore.initialize(config); 400 | 401 | localBroadcastManager = LocalBroadcastManager.getInstance(this); 402 | 403 | String versionName = "?"; 404 | try { 405 | versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; 406 | } catch (PackageManager.NameNotFoundException e) { 407 | e.printStackTrace(); 408 | } 409 | 410 | PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, MapsActivity.class), 0); 411 | 412 | Intent exitIntent = new Intent(this, ClosingActivity.class); 413 | exitIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 414 | exitIntent.putExtra("EXIT", true); 415 | PendingIntent pendingExitIntent = PendingIntent.getActivity(this, 1, exitIntent, PendingIntent.FLAG_UPDATE_CURRENT); 416 | 417 | String CHANNEL_ID = "com.meisterschueler.ognviewer.background"; 418 | String CHANNEL_NAME = "OGN in background"; 419 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { 420 | createNotificationChannel(CHANNEL_ID, CHANNEL_NAME); 421 | } 422 | 423 | Notification notification = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) 424 | .setSmallIcon(R.drawable.ic_stat) 425 | .setContentTitle("OGN Viewer") 426 | .setContentText("Version " + versionName) 427 | .setContentIntent(pendingIntent) 428 | .addAction(R.drawable.ic_stat, "Stop", pendingExitIntent) 429 | .build(); 430 | 431 | startForeground(R.string.notification_id, notification); 432 | } 433 | 434 | @RequiresApi(api = Build.VERSION_CODES.O) 435 | private void createNotificationChannel(String channelId, String channelName) { 436 | NotificationChannel notificationChannel = null; 437 | notificationChannel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW); 438 | notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); 439 | NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 440 | if (manager != null) { 441 | manager.createNotificationChannel(notificationChannel); 442 | } 443 | } 444 | 445 | public float getMovingFilterRange() { 446 | SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 447 | String moving_filter_range = sharedPreferences.getString(getString(R.string.key_movingfilter_range_preference), getString(R.string.distance_10km)); 448 | 449 | float radius = 10000; 450 | if (moving_filter_range.equals(getString(R.string.distance_10km))) { 451 | radius = 10000; 452 | } else if (moving_filter_range.equals(getString(R.string.distance_20km))) { 453 | radius = 20000; 454 | } else if (moving_filter_range.equals(getString(R.string.distance_30km))) { 455 | radius = 30000; 456 | } else if (moving_filter_range.equals(getString(R.string.distance_40km))) { 457 | radius = 40000; 458 | } else if (moving_filter_range.equals(getString(R.string.distance_50km))) { 459 | radius = 50000; 460 | } else if (moving_filter_range.equals(getString(R.string.distance_60km))) { 461 | radius = 60000; 462 | } else if (moving_filter_range.equals(getString(R.string.distance_70km))) { 463 | radius = 70000; 464 | } else if (moving_filter_range.equals(getString(R.string.distance_80km))) { 465 | radius = 80000; 466 | } else if (moving_filter_range.equals(getString(R.string.distance_90km))) { 467 | radius = 90000; 468 | } else if (moving_filter_range.equals(getString(R.string.distance_100km))) { 469 | radius = 100000; 470 | } 471 | return radius; 472 | } 473 | 474 | private void restartAprsClient() { 475 | SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 476 | String aprs_server = sharedPreferences.getString(getString(R.string.key_aprsserver_preference), ""); 477 | String manual_filter = sharedPreferences.getString(getString(R.string.key_aprsfilter_preference), ""); 478 | Boolean moving_filter = sharedPreferences.getBoolean(getString(R.string.key_movingfilter_preference), true); 479 | String moving_filter_range = sharedPreferences.getString(getString(R.string.key_movingfilter_range_preference), getString(R.string.distance_10km)); 480 | 481 | float radius = getMovingFilterRange(); 482 | 483 | if (aprs_server.isEmpty()) { 484 | aprs_server = getString(R.string.default_aprsserver); 485 | } 486 | 487 | if (ognConnected) { 488 | ognClient.disconnect(); 489 | } else { 490 | ognClient = AircraftDescriptorProviderHelper.getOgnClient(aprs_server); 491 | ognClient.subscribeToAircraftBeacons(this); 492 | ognClient.subscribeToReceiverBeacons(this); 493 | customAircraftDescriptorProvider = (CustomAircraftDescriptorProvider)AircraftDescriptorProviderHelper.getCustomDbAircraftDescriptorProvider(); 494 | } 495 | 496 | String aprs_filter; 497 | if (moving_filter) { 498 | if (currentLocation != null) { 499 | String range_filter = AprsFilterManager.latLngToAprsFilter(currentLocation.getLatitude(), currentLocation.getLongitude(), radius); 500 | String buddy_filter = customAircraftDescriptorProvider.getAprsBudlistFilter(); 501 | aprs_filter = range_filter + " " + buddy_filter; 502 | } else { 503 | aprs_filter = manual_filter; 504 | } 505 | 506 | } else { 507 | aprs_filter = manual_filter; 508 | } 509 | 510 | ognClient = AircraftDescriptorProviderHelper.getOgnClient(aprs_server); 511 | ognClient.subscribeToAircraftBeacons(this); 512 | ognClient.subscribeToReceiverBeacons(this); 513 | 514 | if (aprs_filter.isEmpty()) { 515 | ognClient.connect(); 516 | ognConnected = true; 517 | Toast.makeText(this, "Connected to " + aprs_server + " without filter", Toast.LENGTH_LONG).show(); 518 | } else { 519 | ognClient.connect(aprs_filter); 520 | ognConnected = true; 521 | 522 | String filter; 523 | if (aprs_filter.length() > 30) { 524 | filter = aprs_filter.substring(0, 30) + "..."; 525 | } else { 526 | filter = aprs_filter; 527 | } 528 | Toast.makeText(this, "Connected to OGN. Filter: " + filter, Toast.LENGTH_LONG).show(); 529 | } 530 | } 531 | 532 | @Override 533 | public int onStartCommand(Intent intent, int flags, int startId) { 534 | restartAprsClient(); 535 | if (refreshingActive) { 536 | //this happens only when applyModifiedFilter in MapsActivity is called 537 | resumeUpdatingMap(this.latLngBounds); 538 | } 539 | 540 | return START_STICKY; 541 | } 542 | 543 | @Override 544 | public IBinder onBind(Intent intent) { 545 | return binder; 546 | } 547 | 548 | @Override 549 | public void onDestroy() { 550 | super.onDestroy(); 551 | 552 | if (ognConnected) { 553 | ognClient.disconnect(); 554 | ognConnected = false; 555 | } 556 | 557 | Toast.makeText(this, "Disconnected from OGN", Toast.LENGTH_LONG).show(); 558 | 559 | stopLocationUpdates(); 560 | tcpServer.stopServer(); 561 | pauseUpdatingMap(); 562 | } 563 | 564 | //do not delete! WIP from Dominik 2017-11-05 565 | private void updateAircraftBeaconMarkerInDB(String address, AircraftType aircraftType, float climbRate, 566 | double lat, double lon, float alt, float groundSpeed, 567 | String regNumber, String cn, String model, boolean isOgnPrivate, 568 | String receiverName, int track) { 569 | if (!aircraftMap.containsKey(address)) { 570 | Aircraft aircraft = new Aircraft(address, aircraftType, climbRate, lat, lon, alt, groundSpeed, regNumber, 571 | cn, model, isOgnPrivate, receiverName, track); 572 | aircraftMap.put(address, aircraft); 573 | } else { 574 | Aircraft aircraft = aircraftMap.get(address); 575 | aircraft.setAlt(alt); 576 | aircraft.setClimbRate(climbRate); 577 | aircraft.setCN(cn); 578 | aircraft.setGroundSpeed(groundSpeed); 579 | aircraft.setLastSeen(new Date()); 580 | aircraft.setLat(lat); 581 | aircraft.setLon(lon); 582 | aircraft.setReceiverName(receiverName); 583 | aircraft.setTrack(track); 584 | } 585 | 586 | } 587 | 588 | public void resumeUpdatingMap(LatLngBounds latLngBounds) { 589 | this.latLngBounds = latLngBounds; 590 | refreshingActive = true; 591 | if(!ognConnected) { 592 | //This happens when no filter is set and onStartCommand was not called. 593 | return; 594 | } 595 | 596 | if (scheduledTaskExecutor != null) { 597 | Timber.d("Timer was already resumed!"); 598 | return; //This happens when filter is already set and modified in MapsActivity. 599 | } 600 | 601 | final long initialDelayInSeconds = 2L; 602 | final long delayInSeconds = 2L; 603 | scheduledTaskExecutor = new ScheduledThreadPoolExecutor(1); 604 | scheduledTaskExecutor.scheduleWithFixedDelay(new Runnable() { 605 | @Override 606 | public void run() { 607 | //TODO: add try catches (for WIP 2017-11-05) 608 | if (timerCurrentlyRunning) { 609 | Timber.wtf("Two or more timers active!"); 610 | return; //Should never happen! If this happens, two timers are active. 611 | } else { 612 | timerCurrentlyRunning = true; 613 | Timber.d("Update map by timer at %s", new Date()); 614 | } 615 | aircraftMap = convertAircraftMap(aircraftBundleMap); 616 | Iterator it = aircraftMap.keySet().iterator(); 617 | while (it.hasNext()) { 618 | String address = it.next(); 619 | Aircraft aircraft = aircraftMap.get(address); 620 | Date now = new Date(); 621 | long diffSeconds = (now.getTime() - aircraftMap.get(address).getLastSeen().getTime()) / 1000; 622 | // remove markers that are older than specified time e.g. 60 seconds to clean map 623 | if (diffSeconds > aircraftTimeoutInSec) { 624 | Intent intent = new Intent(AppConstants.INTENT_AIRCRAFT_ACTION); 625 | //intent.setAction("REMOVE_AIRCRAFT"); 626 | intent.putExtra("AIRCRAFT_ACTION", "REMOVE_AIRCRAFT"); 627 | intent.putExtra("address", address); 628 | localBroadcastManager.sendBroadcast(intent); 629 | 630 | it.remove(); 631 | aircraftBundleMap.remove(address); 632 | Timber.d("Removed " + address + ", diff: " + diffSeconds + "s"); 633 | continue; 634 | } 635 | //do not delete WIP! 2017-11-05 636 | /*if (latLngBounds.contains(new LatLng(aircraft.getLat(), aircraft.getLon()))) { 637 | while (mapCurrentlyUpdating) { 638 | //wait for finishing updateMaker 639 | System.out.println("Waiting for end of updateMarker: " + address); 640 | } 641 | sendAircraftToMap(aircraftBundleMap.get(address)); //refresh only visible markers 642 | }*/ 643 | //maybe the following code in mapsActivity 644 | /*LatLngBounds latLngBounds = mMap.getProjection().getVisibleRegion().latLngBounds; 645 | if (latLngBounds.contains(new LatLng(aircraft.getLat(), aircraft.getLon()))) { 646 | runOnUiThread(new Runnable() { 647 | @Override 648 | public void run() { 649 | updateAircraftBeaconMarkerOnMap(aircraft.getAddress(), aircraft.getAircraftType(), 650 | aircraft.getClimbRate(), aircraft.getLat(), aircraft.getLon(), 651 | aircraft.getAlt(), aircraft.getGroundSpeed(), aircraft.getRegNumber(), 652 | aircraft.getCN(), aircraft.getModel(), aircraft.isOgnPrivate(), 653 | aircraft.getReceiverName(), aircraft.getTrack()); 654 | } 655 | 656 | }); 657 | }*/ 658 | } 659 | Timber.d("Finished updating map."); 660 | timerCurrentlyRunning = false; 661 | } 662 | }, initialDelayInSeconds, delayInSeconds, TimeUnit.SECONDS); //update aircrafts every few seconds 663 | 664 | Timber.d("Service resumed updating map"); 665 | } 666 | 667 | public void pauseUpdatingMap() { 668 | refreshingActive = false; //blocks intents to activity 669 | if (scheduledTaskExecutor != null) { 670 | scheduledTaskExecutor.shutdownNow(); 671 | scheduledTaskExecutor = null; 672 | } 673 | 674 | timerCurrentlyRunning = false; 675 | 676 | Timber.d("Service paused updating map"); 677 | } 678 | 679 | private Map convertAircraftMap(Map aircraftBundleMap) { 680 | Map resultMap = new HashMap<>(); 681 | for (String address : aircraftBundleMap.keySet()) { 682 | AircraftBundle bundle = aircraftBundleMap.get(address); 683 | AircraftBeacon beacon = bundle.aircraftBeacon; 684 | AircraftDescriptor descriptor = bundle.aircraftDescriptor; 685 | 686 | boolean isOgnPrivate = descriptor.isKnown() && (!descriptor.isTracked() || !descriptor.isIdentified()); 687 | Aircraft aircraft = new Aircraft(beacon.getAddress(), 688 | beacon.getAircraftType(),beacon.getClimbRate(), beacon.getLat(), beacon.getLon(), beacon.getAlt(), beacon.getGroundSpeed(), 689 | descriptor.getRegNumber(), descriptor.getCN(), descriptor.getModel(), isOgnPrivate, beacon.getReceiverName(), beacon.getTrack()); 690 | aircraft.setLastSeen(new Date(beacon.getTimestamp())); 691 | resultMap.put(address, aircraft); 692 | } 693 | 694 | return resultMap; 695 | } 696 | 697 | public class LocalBinder extends Binder { 698 | public OgnService getService() { 699 | return OgnService.this; 700 | } 701 | } 702 | } 703 | --------------------------------------------------------------------------------