diff --git a/app/build.gradle b/app/build.gradle
index 022ab585..222667ed 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -36,8 +36,8 @@ android {
compileSdk 34
minSdk 26
targetSdk 34
- versionCode 84
- versionName "14.7.0"
+ versionCode 85
+ versionName "15.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
diff --git a/app/src/androidTest/java/de/bahnhoefe/deutschlands/bahnhofsfotos/ExampleInstrumentationTest.java b/app/src/androidTest/java/de/bahnhoefe/deutschlands/bahnhofsfotos/ExampleInstrumentationTest.java
deleted file mode 100644
index 93ec7949..00000000
--- a/app/src/androidTest/java/de/bahnhoefe/deutschlands/bahnhofsfotos/ExampleInstrumentationTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.jupiter.api.Test;
-
-/**
- * Instrumentation test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-public class ExampleInstrumentationTest {
-
- @Test
- public void useAppContext() {
- // Context of the app under test.
- var appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
-
- assertThat(appContext.getPackageName()).isEqualTo("de.bahnhoefe.deutschlands.bahnhofsfotos.debug");
- }
-}
\ No newline at end of file
diff --git a/app/src/androidTest/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/TimetableTest.java b/app/src/androidTest/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/TimetableTest.java
deleted file mode 100644
index f3278529..00000000
--- a/app/src/androidTest/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/TimetableTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-
-public class TimetableTest {
-
- private Station station;
-
- @BeforeEach
- public void setUp() {
- station = new Station(
- "de",
- "4711",
- "Some Famous Station",
- 0.0,
- 0.0,
- "LOL");
- }
-
- @Test
- public void createTimetableIntentWithId() {
- var country = new Country("de", "Deutschland", null, "https://example.com/{id}/blah");
- assertThat(new Timetable().createTimetableIntent(country, station).getData().toString()).isEqualTo("https://example.com/4711/blah");
- }
-
- @Test
- public void createTimetableIntentWithTitle() {
- var country = new Country("de", "Deutschland", null, "https://example.com/{title}/blah");
- assertThat(new Timetable().createTimetableIntent(country, station).getData().toString()).isEqualTo("https://example.com/Some Famous Station/blah");
- }
-
- @Test
- public void createTimetableIntentWithDS100() {
- var country = new Country("de", "Deutschland", null, "https://example.com/{DS100}/blah");
- assertThat(new Timetable().createTimetableIntent(country, station).getData().toString()).isEqualTo("https://example.com/LOL/blah");
- }
-
-}
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/ExampleInstrumentationTest.kt b/app/src/androidTest/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/ExampleInstrumentationTest.kt
new file mode 100644
index 00000000..e0c8a9ed
--- /dev/null
+++ b/app/src/androidTest/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/ExampleInstrumentationTest.kt
@@ -0,0 +1,20 @@
+package de.bahnhoefe.deutschlands.bahnhofsfotos
+
+import androidx.test.platform.app.InstrumentationRegistry
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see [Testing documentation](http://d.android.com/tools/testing)
+ */
+class ExampleInstrumentationTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertThat(appContext.packageName)
+ .isEqualTo("de.bahnhoefe.deutschlands.bahnhofsfotos.debug")
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/util/TimetableTest.kt b/app/src/androidTest/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/util/TimetableTest.kt
new file mode 100644
index 00000000..403d2aee
--- /dev/null
+++ b/app/src/androidTest/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/util/TimetableTest.kt
@@ -0,0 +1,48 @@
+package de.bahnhoefe.deutschlands.bahnhofsfotos.util
+
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class TimetableTest {
+ private var station: Station? = null
+
+ @BeforeEach
+ fun setUp() {
+ station = Station(
+ "de",
+ "4711",
+ "Some Famous Station",
+ 0.0,
+ 0.0,
+ "LOL",
+ photoId = null,
+ )
+ }
+
+ @Test
+ fun createTimetableIntentWithId() {
+ val country = Country("de", "Deutschland", null, "https://example.com/{id}/blah")
+ assertThat(
+ Timetable().createTimetableIntent(country, station)!!.data.toString()
+ ).isEqualTo("https://example.com/4711/blah")
+ }
+
+ @Test
+ fun createTimetableIntentWithTitle() {
+ val country = Country("de", "Deutschland", null, "https://example.com/{title}/blah")
+ assertThat(
+ Timetable().createTimetableIntent(country, station)!!.data.toString()
+ ).isEqualTo("https://example.com/Some Famous Station/blah")
+ }
+
+ @Test
+ fun createTimetableIntentWithDS100() {
+ val country = Country("de", "Deutschland", null, "https://example.com/{DS100}/blah")
+ assertThat(
+ Timetable().createTimetableIntent(country, station)!!.data.toString()
+ ).isEqualTo("https://example.com/LOL/blah")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/BaseApplication.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/BaseApplication.java
deleted file mode 100644
index 269ea138..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/BaseApplication.java
+++ /dev/null
@@ -1,444 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import android.annotation.SuppressLint;
-import android.app.Application;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.location.Location;
-import android.net.Uri;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.multidex.MultiDex;
-import androidx.security.crypto.EncryptedSharedPreferences;
-import androidx.security.crypto.MasterKey;
-
-import org.apache.commons.lang3.StringUtils;
-import org.mapsforge.core.model.LatLong;
-import org.mapsforge.core.model.MapPosition;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Optional;
-import java.util.Set;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.db.DbAdapter;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.License;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Profile;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.UpdatePolicy;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi.RSAPIClient;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.ExceptionHandler;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.StationFilter;
-
-public class BaseApplication extends Application {
-
- private static final String TAG = BaseApplication.class.getSimpleName();
- private static final Boolean DEFAULT_FIRSTAPPSTART = false;
- private static final String DEFAULT = "";
- private static BaseApplication instance;
-
- public static final String DEFAULT_COUNTRY = "de";
- public static final String PREF_FILE = "APP_PREF_FILE";
-
- private DbAdapter dbAdapter;
- private RSAPIClient rsapiClient;
- private SharedPreferences preferences;
- private SharedPreferences encryptedPreferences;
-
- public BaseApplication() {
- setInstance(this);
- }
-
- public DbAdapter getDbAdapter() {
- return dbAdapter;
- }
-
- @Override
- protected void attachBaseContext(Context base) {
- super.attachBaseContext(base);
- MultiDex.install(this);
-
- // handle crashes only outside the crash reporter activity/process
- if (!isCrashReportingProcess()) {
- Thread.setDefaultUncaughtExceptionHandler(
- new ExceptionHandler(this, Thread.getDefaultUncaughtExceptionHandler()));
- }
- }
-
- private boolean isCrashReportingProcess() {
- var processName = "";
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
- // Using the same technique as Application.getProcessName() for older devices
- // Using reflection since ActivityThread is an internal API
- try {
- @SuppressLint("PrivateApi")
- var activityThread = Class.forName("android.app.ActivityThread");
- @SuppressLint("DiscouragedPrivateApi")
- var getProcessName = activityThread.getDeclaredMethod("currentProcessName");
- processName = (String) getProcessName.invoke(null);
- } catch (Exception ignored) {
- }
- } else {
- processName = Application.getProcessName();
- }
- return processName != null && processName.endsWith(":crash");
- }
-
- private static void setInstance(@NonNull BaseApplication application) {
- instance = application;
- }
-
- public static BaseApplication getInstance() {
- return instance;
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- dbAdapter = new DbAdapter(this);
- dbAdapter.open();
-
- preferences = getSharedPreferences(PREF_FILE, MODE_PRIVATE);
-
- // Creates the instance for the encrypted preferences.
- encryptedPreferences = null;
- try {
- var masterKey = new MasterKey.Builder(this)
- .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
- .build();
-
- encryptedPreferences = EncryptedSharedPreferences.create(
- this,
- "secret_shared_prefs",
- masterKey,
- EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
- EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
- );
-
- // migrate access token from unencrypted to encrypted preferences
- if (!encryptedPreferences.contains(getString(R.string.ACCESS_TOKEN))
- && preferences.contains(getString(R.string.ACCESS_TOKEN))) {
- setAccessToken(preferences.getString(getString(R.string.ACCESS_TOKEN), null));
- preferences.edit().remove(getString(R.string.ACCESS_TOKEN)).apply();
- }
- } catch (GeneralSecurityException | IOException e) {
- Log.w("Unable to create EncryptedSharedPreferences, fallback to unencrypted preferences", e);
-
- }
-
- // migrate photo owner preference to boolean
- var photoOwner = preferences.getAll().get(getString(R.string.PHOTO_OWNER));
- if ("YES".equals(photoOwner)) {
- setPhotoOwner(true);
- }
-
- rsapiClient = new RSAPIClient(getApiUrl(), getString(R.string.rsapiClientId), getAccessToken(), getString(R.string.rsapiRedirectScheme) + "://" + getString(R.string.rsapiRedirectHost));
- }
-
- public String getApiUrl() {
- var apiUri = preferences.getString(getString(R.string.API_URL), null);
- return getValidatedApiUrlString(apiUri);
- }
-
- private static String getValidatedApiUrlString(final String apiUrl) {
- var uri = toUri(apiUrl);
- if (uri.isPresent()) {
- var scheme = uri.get().getScheme();
- if (scheme != null && scheme.matches("https?")) {
- return apiUrl + (apiUrl.endsWith("/") ? "" : "/");
- }
- }
-
- return "https://api.railway-stations.org/";
- }
-
- public void setApiUrl(String apiUrl) {
- var validatedUrl = getValidatedApiUrlString(apiUrl);
- putString(R.string.API_URL, validatedUrl);
- rsapiClient.setBaseUrl(validatedUrl);
- }
-
- private void putBoolean(int key, boolean value) {
- var editor = preferences.edit();
- editor.putBoolean(getString(key), value);
- editor.apply();
- }
-
- private void putString(int key, String value) {
- var editor = preferences.edit();
- editor.putString(getString(key), StringUtils.trimToNull(value));
- editor.apply();
- }
-
- private void putStringSet(int key, Set value) {
- var editor = preferences.edit();
- editor.putStringSet(getString(key), value);
- editor.apply();
- }
-
- private void putLong(int key, long value) {
- var editor = preferences.edit();
- editor.putLong(getString(key), value);
- editor.apply();
- }
-
- private void putDouble(int key, double value) {
- var editor = preferences.edit();
- editor.putLong(getString(key), Double.doubleToRawLongBits(value));
- editor.apply();
- }
-
- private double getDouble(int key) {
- if (!preferences.contains(getString(key))) {
- return 0.0;
- }
-
- return Double.longBitsToDouble(preferences.getLong(getString(key), 0));
- }
-
- public void setCountryCodes(Set countryCodes) {
- putStringSet(R.string.COUNTRIES, countryCodes);
- }
-
- public Set getCountryCodes() {
- var oldCountryCode = preferences.getString(getString(R.string.COUNTRY), DEFAULT_COUNTRY);
- var stringSet = preferences.getStringSet(getString(R.string.COUNTRIES), new HashSet<>(Collections.singleton(oldCountryCode)));
- if (stringSet.isEmpty()) {
- stringSet = new HashSet<>(Collections.singleton(DEFAULT_COUNTRY));
- }
- return stringSet;
- }
-
- public void setFirstAppStart(boolean firstAppStart) {
- putBoolean(R.string.FIRSTAPPSTART, firstAppStart);
- }
-
- public boolean getFirstAppStart() {
- return preferences.getBoolean(getString(R.string.FIRSTAPPSTART), DEFAULT_FIRSTAPPSTART);
- }
-
- public License getLicense() {
- return License.byName(preferences.getString(getString(R.string.LICENCE), License.UNKNOWN.toString()));
- }
-
- public void setLicense(License license) {
- putString(R.string.LICENCE, license != null ? license.toString() : License.UNKNOWN.toString());
- }
-
- public UpdatePolicy getUpdatePolicy() {
- return UpdatePolicy.byName(preferences.getString(getString(R.string.UPDATE_POLICY), License.UNKNOWN.toString()));
- }
-
- public void setUpdatePolicy(UpdatePolicy updatePolicy) {
- putString(R.string.UPDATE_POLICY, updatePolicy.toString());
- }
-
- public boolean getPhotoOwner() {
- return preferences.getBoolean(getString(R.string.PHOTO_OWNER), false);
- }
-
- public void setPhotoOwner(boolean photoOwner) {
- putBoolean(R.string.PHOTO_OWNER, photoOwner);
- }
-
- public String getPhotographerLink() {
- return preferences.getString(getString(R.string.LINK_TO_PHOTOGRAPHER), DEFAULT);
- }
-
- public void setPhotographerLink(String photographerLink) {
- putString(R.string.LINK_TO_PHOTOGRAPHER, photographerLink);
- }
-
- public String getNickname() {
- return preferences.getString(getString(R.string.NICKNAME), DEFAULT);
- }
-
- public void setNickname(String nickname) {
- putString(R.string.NICKNAME, nickname);
- }
-
- public String getEmail() {
- return preferences.getString(getString(R.string.EMAIL), DEFAULT);
- }
-
- public void setEmail(String email) {
- putString(R.string.EMAIL, email);
- }
-
- public boolean isEmailVerified() {
- return preferences.getBoolean(getString(R.string.PHOTO_OWNER), false);
- }
-
- public void setEmailVerified(boolean emailVerified) {
- putBoolean(R.string.PHOTO_OWNER, emailVerified);
- }
-
- public String getAccessToken() {
- return encryptedPreferences.getString(getString(R.string.ACCESS_TOKEN), null);
- }
-
- public void setAccessToken(String apiToken) {
- var editor = encryptedPreferences.edit();
- editor.putString(getString(R.string.ACCESS_TOKEN), StringUtils.trimToNull(apiToken));
- editor.apply();
- }
-
- public StationFilter getStationFilter() {
- var photoFilter = getOptionalBoolean(R.string.STATION_FILTER_PHOTO);
- var activeFilter = getOptionalBoolean(R.string.STATION_FILTER_ACTIVE);
- var nicknameFilter = preferences.getString(getString(R.string.STATION_FILTER_NICKNAME), null);
- return new StationFilter(photoFilter, activeFilter, nicknameFilter);
- }
-
- private Boolean getOptionalBoolean(int key) {
- if (preferences.contains(getString(key))) {
- return Boolean.valueOf(preferences.getString(getString(key), "false"));
- }
- return null;
- }
-
- public void setStationFilter(StationFilter stationFilter) {
- putString(R.string.STATION_FILTER_PHOTO, stationFilter.hasPhoto() == null ? null : stationFilter.hasPhoto().toString());
- putString(R.string.STATION_FILTER_ACTIVE, stationFilter.isActive() == null ? null : stationFilter.isActive().toString());
- putString(R.string.STATION_FILTER_NICKNAME, stationFilter.getNickname());
- }
-
- public long getLastUpdate() {
- return preferences.getLong(getString(R.string.LAST_UPDATE), 0L);
- }
-
- public void setLastUpdate(long lastUpdate) {
- putLong(R.string.LAST_UPDATE, lastUpdate);
- }
-
- public void setLocationUpdates(boolean locationUpdates) {
- putBoolean(R.string.LOCATION_UPDATES, locationUpdates);
- }
-
- public boolean isLocationUpdates() {
- return preferences.getBoolean(getString(R.string.LOCATION_UPDATES), true);
- }
-
- public void setLastMapPosition(MapPosition lastMapPosition) {
- putDouble(R.string.LAST_POSITION_LAT, lastMapPosition.latLong.latitude);
- putDouble(R.string.LAST_POSITION_LON, lastMapPosition.latLong.longitude);
- putLong(R.string.LAST_POSITION_ZOOM, lastMapPosition.zoomLevel);
- }
-
- public MapPosition getLastMapPosition() {
- var latLong = new LatLong(getDouble(R.string.LAST_POSITION_LAT), getDouble(R.string.LAST_POSITION_LON));
- return new MapPosition(latLong, (byte) preferences.getLong(getString(R.string.LAST_POSITION_ZOOM), getZoomLevelDefault()));
- }
-
- public Location getLastLocation() {
- var location = new Location("");
- location.setLatitude(getDouble(R.string.LAST_POSITION_LAT));
- location.setLongitude(getDouble(R.string.LAST_POSITION_LON));
- return location;
- }
-
- /**
- * @return the default starting zoom level if nothing is encoded in the map file.
- */
- public byte getZoomLevelDefault() {
- return (byte) 12;
- }
-
- public boolean getAnonymous() {
- return preferences.getBoolean(getString(R.string.ANONYMOUS), false);
- }
-
- public void setAnonymous(boolean anonymous) {
- putBoolean(R.string.ANONYMOUS, anonymous);
- }
-
- public void setProfile(Profile profile) {
- setLicense(profile.getLicense());
- setPhotoOwner(profile.getPhotoOwner());
- setAnonymous(profile.getAnonymous());
- setPhotographerLink(profile.getLink());
- setNickname(profile.getNickname());
- setEmail(profile.getEmail());
- setEmailVerified(profile.getEmailVerified());
- }
-
- public Profile getProfile() {
- return new Profile(
- getNickname(),
- getLicense(),
- getPhotoOwner(),
- getAnonymous(),
- getPhotographerLink(),
- getEmail(),
- isEmailVerified());
- }
-
- public RSAPIClient getRsapiClient() {
- return rsapiClient;
- }
-
- public String getMap() {
- return preferences.getString(getString(R.string.MAP_FILE), null);
- }
-
- public void setMap(String map) {
- putString(R.string.MAP_FILE, map);
- }
-
- private void putUri(int key, Uri uri) {
- putString(key, uri != null ? uri.toString() : null);
- }
-
- public Optional getMapDirectoryUri() {
- return getUri(getString(R.string.MAP_DIRECTORY));
- }
-
- private Optional getUri(String key) {
- return toUri(preferences.getString(key, null));
- }
-
- public static Optional toUri(String uriString) {
- try {
- return Optional.ofNullable(Uri.parse(uriString));
- } catch (Exception ignored) {
- Log.e(TAG, "can't read Uri string " + uriString);
- }
- return Optional.empty();
- }
-
- public void setMapDirectoryUri(Uri mapDirectory) {
- putUri(R.string.MAP_DIRECTORY, mapDirectory);
- }
-
- public Optional getMapThemeDirectoryUri() {
- return getUri(getString(R.string.MAP_THEME_DIRECTORY));
- }
-
- public void setMapThemeDirectoryUri(Uri mapThemeDirectory) {
- putUri(R.string.MAP_THEME_DIRECTORY, mapThemeDirectory);
- }
-
- public Optional getMapThemeUri() {
- return getUri(getString(R.string.MAP_THEME));
- }
-
- public void setMapThemeUri(Uri mapTheme) {
- putUri(R.string.MAP_THEME, mapTheme);
- }
-
- public boolean getSortByDistance() {
- return preferences.getBoolean(getString(R.string.SORT_BY_DISTANCE), false);
- }
-
- public void setSortByDistance(boolean sortByDistance) {
- putBoolean(R.string.SORT_BY_DISTANCE, sortByDistance);
- }
-
- public boolean isLoggedIn() {
- return rsapiClient.hasToken();
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/CountryActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/CountryActivity.java
deleted file mode 100644
index ce223d83..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/CountryActivity.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import android.os.Bundle;
-import android.view.MenuItem;
-
-import androidx.appcompat.app.AppCompatActivity;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityCountryBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.db.CountryAdapter;
-
-public class CountryActivity extends AppCompatActivity {
-
- private CountryAdapter countryAdapter;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- var binding = ActivityCountryBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- var cursor = ((BaseApplication) getApplication()).getDbAdapter().getCountryList();
- countryAdapter = new CountryAdapter(this, cursor, 0);
- binding.lstCountries.setAdapter(countryAdapter);
- binding.lstCountries.setOnItemClickListener((listview, view, position, id) -> countryAdapter.getView(position, view, binding.lstCountries, cursor));
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == android.R.id.home) {
- onBackPressed();
- return true;
- }
- return false;
- }
-
- @Override
- public void onBackPressed() {
- var baseApplication = BaseApplication.getInstance();
- var selectedCountries = countryAdapter.getSelectedCountries();
-
- if (!baseApplication.getCountryCodes().equals(selectedCountries)) {
- baseApplication.setCountryCodes(selectedCountries);
- baseApplication.setLastUpdate(0L);
- }
- super.onBackPressed();
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/DetailsActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/DetailsActivity.java
deleted file mode 100644
index 04529175..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/DetailsActivity.java
+++ /dev/null
@@ -1,551 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import static android.content.Intent.ACTION_VIEW;
-import static android.content.Intent.createChooser;
-
-import android.app.TaskStackBuilder;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.net.Uri;
-import android.os.Bundle;
-import android.text.Html;
-import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
-import android.util.Log;
-import android.view.ContextThemeWrapper;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.activity.OnBackPressedCallback;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.ActivityCompat;
-import androidx.core.app.NavUtils;
-import androidx.core.content.ContextCompat;
-import androidx.core.content.FileProvider;
-import androidx.viewpager2.widget.ViewPager2;
-
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityDetailsBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.StationInfoBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PageablePhoto;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Photo;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PhotoStations;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.ProviderApp;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Upload;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi.RSAPIClient;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.BitmapCache;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.ConnectionUtil;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.Constants;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.FileUtils;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.NavItem;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.Timetable;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-public class DetailsActivity extends AppCompatActivity implements ActivityCompat.OnRequestPermissionsResultCallback {
-
- private static final String TAG = DetailsActivity.class.getSimpleName();
-
- // Names of Extras that this class reacts to
- public static final String EXTRA_STATION = "EXTRA_STATION";
-
- private static final String LINK_FORMAT = "%s";
-
- private BaseApplication baseApplication;
- private RSAPIClient rsapiClient;
- private ActivityDetailsBinding binding;
- private Station station;
- private Set countries;
- private String nickname;
- private PhotoPagerAdapter photoPagerAdapter;
- private final Map photoBitmaps = new HashMap<>();
- private PageablePhoto selectedPhoto;
- private final List carouselPageIndicators = new ArrayList<>();
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- binding = ActivityDetailsBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- baseApplication = (BaseApplication) getApplication();
- rsapiClient = baseApplication.getRsapiClient();
- countries = baseApplication.getDbAdapter().fetchCountriesWithProviderApps(baseApplication.getCountryCodes());
-
- Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
-
- photoPagerAdapter = new PhotoPagerAdapter(this);
- binding.details.viewPager.setAdapter(photoPagerAdapter);
- binding.details.viewPager.setCurrentItem(0, false);
- binding.details.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
- @Override
- public void onPageSelected(int position) {
- var pageablePhoto = photoPagerAdapter.getPageablePhotoAtPosition(position);
- onPageablePhotoSelected(pageablePhoto, position);
- }
- });
-
- // switch off image and license view until we actually have a foto
- binding.details.licenseTag.setVisibility(View.INVISIBLE);
- binding.details.licenseTag.setMovementMethod(LinkMovementMethod.getInstance());
-
- getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
- @Override
- public void handleOnBackPressed() {
- navigateUp();
- }
- });
-
- readPreferences();
- onNewIntent(getIntent());
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
-
- if (intent != null) {
- station = (Station) intent.getSerializableExtra(EXTRA_STATION);
-
- if (station == null) {
- Log.w(TAG, "EXTRA_STATION in intent data missing");
- Toast.makeText(this, R.string.station_not_found, Toast.LENGTH_LONG).show();
- finish();
- return;
- }
-
- binding.details.marker.setImageDrawable(ContextCompat.getDrawable(this, getMarkerRes()));
-
- binding.details.tvStationTitle.setText(station.getTitle());
- binding.details.tvStationTitle.setSingleLine(false);
-
- if (station.hasPhoto()) {
- if (ConnectionUtil.checkInternetConnection(this)) {
- photoBitmaps.put(station.getPhotoUrl(), null);
- BitmapCache.getInstance().getPhoto((bitmap) -> {
- if (bitmap != null) {
- var pageablePhoto = new PageablePhoto(station, bitmap);
- runOnUiThread(() -> {
- addIndicator();
- var position = photoPagerAdapter.addPageablePhoto(pageablePhoto);
- if (position == 0) {
- onPageablePhotoSelected(pageablePhoto, position);
- }
- });
- }
- }, station.getPhotoUrl());
- }
- }
-
- loadAdditionalPhotos(station);
-
- baseApplication.getDbAdapter()
- .getPendingUploadsForStation(station)
- .forEach(this::addUploadPhoto);
- }
-
- }
-
- private void addUploadPhoto(final Upload upload) {
- if (!upload.isPendingPhotoUpload()) {
- return;
- }
- var profile = baseApplication.getProfile();
- var file = FileUtils.getStoredMediaFile(this, upload.getId());
- if (file != null && file.canRead()) {
- var bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
- if (bitmap != null) {
- var pageablePhoto = new PageablePhoto(
- upload.getId(),
- file.toURI().toString(),
- getString(R.string.new_local_photo),
- "",
- profile.getLicense() != null ? profile.getLicense().getLongName() : "",
- "",
- bitmap);
- runOnUiThread(() -> {
- addIndicator();
- var position = photoPagerAdapter.addPageablePhoto(pageablePhoto);
- if (position == 0) {
- onPageablePhotoSelected(pageablePhoto, position);
- }
- });
- }
- }
- }
-
- private void loadAdditionalPhotos(final Station station) {
- rsapiClient.getPhotoStationById(station.getCountry(), station.getId()).enqueue(new Callback<>() {
-
- @Override
- public void onResponse(@NonNull final Call call, @NonNull final Response response) {
- if (response.isSuccessful()) {
- var photoStations = response.body();
- if (photoStations == null) {
- return;
- }
- photoStations.getStations().stream()
- .flatMap(photoStation -> photoStation.getPhotos().stream())
- .forEach(photo -> {
- var url = photoStations.getPhotoBaseUrl() + photo.getPath();
- if (!photoBitmaps.containsKey(url)) {
- photoBitmaps.put(url, null);
- addIndicator();
- BitmapCache.getInstance().getPhoto((bitmap) -> runOnUiThread(() -> addAdditionalPhotoToPagerAdapter(
- photo,
- url,
- photoStations,
- bitmap)), url);
- }
- });
- }
- }
-
- @Override
- public void onFailure(@NonNull final Call call, @NonNull final Throwable t) {
- Log.e(TAG, "Failed to load additional photos", t);
- }
- });
- }
-
- private void addAdditionalPhotoToPagerAdapter(Photo photo, String url, PhotoStations photoStations, Bitmap bitmap) {
- photoPagerAdapter.addPageablePhoto(
- new PageablePhoto(
- photo.getId(),
- url,
- photo.getPhotographer(),
- photoStations.getPhotographerUrl(photo.getPhotographer()),
- photoStations.getLicenseName(photo.getLicense()),
- photoStations.getLicenseUrl(photo.getLicense()),
- bitmap)
- );
- }
-
- private void addIndicator() {
- var indicator = new ImageView(DetailsActivity.this);
- indicator.setImageResource(R.drawable.selector_carousel_page_indicator);
- indicator.setPadding(0, 0, 5, 0); // left, top, right, bottom
- binding.details.llPageIndicatorContainer.addView(indicator);
- carouselPageIndicators.add(indicator);
- }
-
- private int getMarkerRes() {
- if (station == null) {
- return R.drawable.marker_missing;
- }
- if (station.hasPhoto()) {
- if (isOwner()) {
- return station.getActive() ? R.drawable.marker_violet : R.drawable.marker_violet_inactive;
- } else {
- return station.getActive() ? R.drawable.marker_green : R.drawable.marker_green_inactive;
- }
- } else {
- return station.getActive() ? R.drawable.marker_red : R.drawable.marker_red_inactive;
- }
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- readPreferences();
- }
-
- private void readPreferences() {
- nickname = baseApplication.getNickname();
- }
-
- private boolean isOwner() {
- return station != null && TextUtils.equals(nickname, station.getPhotographer());
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.details, menu);
- return super.onCreateOptionsMenu(menu);
- }
-
- ActivityResultLauncher activityForResultLauncher = registerForActivityResult(
- new ActivityResultContracts.StartActivityForResult(),
- result -> recreate());
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- int itemId = item.getItemId();
- if (itemId == R.id.add_photo) {
- var intent = new Intent(DetailsActivity.this, UploadActivity.class);
- intent.putExtra(UploadActivity.EXTRA_STATION, station);
- activityForResultLauncher.launch(intent);
- } else if (itemId == R.id.report_problem) {
- var intent = new Intent(DetailsActivity.this, ProblemReportActivity.class);
- intent.putExtra(ProblemReportActivity.EXTRA_STATION, station);
- intent.putExtra(ProblemReportActivity.EXTRA_PHOTO_ID, selectedPhoto != null ? selectedPhoto.getId() : null);
- startActivity(intent);
- } else if (itemId == R.id.nav_to_station) {
- startNavigation(DetailsActivity.this);
- } else if (itemId == R.id.timetable) {
- Country.getCountryByCode(countries, station.getCountry()).map(country -> {
- var timetableIntent = new Timetable().createTimetableIntent(country, station);
- if (timetableIntent != null) {
- startActivity(timetableIntent);
- }
- return null;
- });
- } else if (itemId == R.id.share_link) {
- var stationUri = Uri.parse(String.format("https://map.railway-stations.org/station.php?countryCode=%s&stationId=%s", station.getCountry(), station.getId()));
- startActivity(new Intent(ACTION_VIEW, stationUri));
- } else if (itemId == R.id.share_photo) {
- Country.getCountryByCode(countries, station.getCountry()).map(country -> {
- var shareIntent = createPhotoSendIntent();
- if (shareIntent == null) {
- return null;
- }
- shareIntent.putExtra(Intent.EXTRA_TEXT, binding.details.tvStationTitle.getText());
- shareIntent.setType("image/jpeg");
- startActivity(createChooser(shareIntent, "send"));
- return null;
- });
- } else if (itemId == R.id.station_info) {
- showStationInfo(null);
- } else if (itemId == R.id.provider_android_app) {
- Country.getCountryByCode(countries, station.getCountry()).map(country -> {
- var providerApps = country.getCompatibleProviderApps();
- if (providerApps.size() == 1) {
- openAppOrPlayStore(providerApps.get(0), this);
- } else if (providerApps.size() > 1) {
- var appNames = providerApps.stream()
- .map(ProviderApp::getName).toArray(CharSequence[]::new);
- SimpleDialogs.simpleSelect(this, getResources().getString(R.string.choose_provider_app), appNames, (dialog, which) -> {
- if (which >= 0 && providerApps.size() > which) {
- openAppOrPlayStore(providerApps.get(which), DetailsActivity.this);
- }
- });
- } else {
- Toast.makeText(this, R.string.provider_app_missing, Toast.LENGTH_LONG).show();
- }
- return null;
- });
- } else if (itemId == android.R.id.home) {
- navigateUp();
- } else {
- return super.onOptionsItemSelected(item);
- }
-
- return true;
- }
-
- /**
- * Tries to open the provider app if installed. If it is not installed or cannot be opened Google Play Store will be opened instead.
- *
- * @param context activity context
- */
- public void openAppOrPlayStore(ProviderApp providerApp, Context context) {
- // Try to open App
- boolean success = openApp(providerApp, context);
- // Could not open App, open play store instead
- if (!success) {
- var intent = new Intent(ACTION_VIEW);
- intent.setData(Uri.parse(providerApp.getUrl()));
- context.startActivity(intent);
- }
- }
-
- /**
- * Open another app.
- *
- * @param context activity context
- * @return true if likely successful, false if unsuccessful
- * @see https://stackoverflow.com/a/7596063/714965
- */
- @SuppressWarnings("JavadocReference")
- private boolean openApp(ProviderApp providerApp, Context context) {
- if (!providerApp.isAndroid()) {
- return false;
- }
- var manager = context.getPackageManager();
- try {
- String packageName = Uri.parse(providerApp.getUrl()).getQueryParameter("id");
- assert packageName != null;
- var intent = manager.getLaunchIntentForPackage(packageName);
- if (intent == null) {
- return false;
- }
- intent.addCategory(Intent.CATEGORY_LAUNCHER);
- context.startActivity(intent);
- return true;
- } catch (ActivityNotFoundException e) {
- return false;
- }
- }
-
- public void navigateUp() {
- var callingActivity = getCallingActivity(); // if MapsActivity was calling, then we don't want to rebuild the Backstack
- var upIntent = NavUtils.getParentActivityIntent(this);
- if (callingActivity == null && upIntent != null) {
- upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
- if (NavUtils.shouldUpRecreateTask(this, upIntent) || isTaskRoot()) {
- Log.v(TAG, "Recreate back stack");
- TaskStackBuilder.create(this).addNextIntentWithParentStack(upIntent).startActivities();
- }
- }
-
- finish();
- }
-
- public void showStationInfo(View view) {
- var stationInfoBinding = StationInfoBinding.inflate(getLayoutInflater());
- stationInfoBinding.id.setText(station.getId());
- stationInfoBinding.coordinates.setText(String.format(Locale.US, getResources().getString(R.string.coordinates), station.getLat(), station.getLon()));
- stationInfoBinding.active.setText(station != null && station.getActive() ? R.string.active : R.string.inactive);
- stationInfoBinding.owner.setText(station != null && station.getPhotographer() != null ? station.getPhotographer() : "");
- if (station.getOutdated()) {
- stationInfoBinding.outdatedLabel.setVisibility(View.VISIBLE);
- }
-
- new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AlertDialogCustom))
- .setTitle(binding.details.tvStationTitle.getText())
- .setView(stationInfoBinding.getRoot())
- .setIcon(R.mipmap.ic_launcher)
- .setPositiveButton(android.R.string.ok, null)
- .create()
- .show();
- }
-
- private Intent createPhotoSendIntent() {
- if (selectedPhoto != null) {
- var sendIntent = new Intent(Intent.ACTION_SEND);
- var newFile = FileUtils.getImageCacheFile(getApplicationContext(), String.valueOf(System.currentTimeMillis()));
- try {
- Log.i(TAG, "Save photo to: " + newFile);
- selectedPhoto.getBitmap().compress(Bitmap.CompressFormat.JPEG, Constants.STORED_PHOTO_QUALITY, new FileOutputStream(newFile));
- sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(DetailsActivity.this,
- BuildConfig.APPLICATION_ID + ".fileprovider", newFile));
- return sendIntent;
- } catch (FileNotFoundException e) {
- Log.e(TAG, "Error saving cached bitmap", e);
- }
- }
- return null;
- }
-
- private void startNavigation(Context context) {
- var adapter = new ArrayAdapter<>(this, android.R.layout.select_dialog_item,
- android.R.id.text1, NavItem.values()) {
- @NonNull
- public View getView(int position, View convertView, @NonNull ViewGroup parent) {
- var item = getItem(position);
- assert item != null;
-
- var view = super.getView(position, convertView, parent);
- TextView tv = view.findViewById(android.R.id.text1);
-
- //Put the image on the TextView
- tv.setCompoundDrawablesWithIntrinsicBounds(item.getIconRes(), 0, 0, 0);
- tv.setText(getString(item.getTextRes()));
-
- //Add margin between image and text (support various screen densities)
- int dp5 = (int) (20 * getResources().getDisplayMetrics().density + 0.5f);
- int dp7 = (int) (20 * getResources().getDisplayMetrics().density);
- tv.setCompoundDrawablePadding(dp5);
- tv.setPadding(dp7, 0, 0, 0);
-
- return view;
- }
- };
-
- new AlertDialog.Builder(this)
- .setIcon(R.mipmap.ic_launcher)
- .setTitle(R.string.navMethod)
- .setAdapter(adapter, (dialog, position) -> {
- var item = adapter.getItem(position);
- assert item != null;
- var lat = station.getLat();
- var lon = station.getLon();
- var intent = item.createIntent(DetailsActivity.this, lat, lon, binding.details.tvStationTitle.getText().toString(), getMarkerRes());
- try {
- startActivity(intent);
- } catch (Exception e) {
- Toast.makeText(context, R.string.activitynotfound, Toast.LENGTH_LONG).show();
- }
- }).show();
- }
-
- public void onPageablePhotoSelected(PageablePhoto pageablePhoto, int position) {
- selectedPhoto = pageablePhoto;
- binding.details.licenseTag.setVisibility(View.INVISIBLE);
-
- if (pageablePhoto == null) {
- return;
- }
-
- // Lizenzinfo aufbauen und einblenden
- binding.details.licenseTag.setVisibility(View.VISIBLE);
- boolean photographerUrlAvailable = pageablePhoto.getPhotographerUrl() != null && !pageablePhoto.getPhotographerUrl().isEmpty();
- boolean licenseUrlAvailable = pageablePhoto.getLicenseUrl() != null && !pageablePhoto.getLicenseUrl().isEmpty();
-
- String photographerText;
- if (photographerUrlAvailable) {
- photographerText = String.format(
- LINK_FORMAT,
- pageablePhoto.getPhotographerUrl(),
- pageablePhoto.getPhotographer());
- } else {
- photographerText = pageablePhoto.getPhotographer();
- }
-
- String licenseText;
- if (licenseUrlAvailable) {
- licenseText = String.format(
- LINK_FORMAT,
- pageablePhoto.getLicenseUrl(),
- pageablePhoto.getLicense());
- } else {
- licenseText = pageablePhoto.getLicense();
- }
-
- binding.details.licenseTag.setText(
- Html.fromHtml(
- String.format(
- getText(R.string.license_tag).toString(),
- photographerText,
- licenseText), Html.FROM_HTML_MODE_LEGACY
- )
- );
-
- if (carouselPageIndicators != null) {
- for (int i = 0; i < carouselPageIndicators.size(); i++) {
- if (i == position) {
- carouselPageIndicators.get(position).setSelected(true);
- } else {
- carouselPageIndicators.get(i).setSelected(false);
- }
- }
- }
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/HighScoreActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/HighScoreActivity.java
deleted file mode 100644
index b743154d..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/HighScoreActivity.java
+++ /dev/null
@@ -1,134 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import android.app.SearchManager;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.Menu;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.SearchView;
-
-import java.util.ArrayList;
-import java.util.Collections;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityHighScoreBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.db.HighScoreAdapter;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.HighScore;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.HighScoreItem;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-public class HighScoreActivity extends AppCompatActivity {
-
- private static final String TAG = "HighScoreActivity";
- private HighScoreAdapter adapter;
- private ActivityHighScoreBinding binding;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- binding = ActivityHighScoreBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- var baseApplication = (BaseApplication) getApplication();
- var firstSelectedCountry = baseApplication.getCountryCodes().iterator().next();
- var countries = new ArrayList<>(baseApplication.getDbAdapter().getAllCountries());
- Collections.sort(countries);
- countries.add(0, new Country("", getString(R.string.all_countries)));
- int selectedItem = 0;
- for (var country : countries) {
- if (country.getCode().equals(firstSelectedCountry)) {
- selectedItem = countries.indexOf(country);
- }
- }
-
- var countryAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, countries.toArray(new Country[0]));
- binding.countries.setAdapter(countryAdapter);
- binding.countries.setSelection(selectedItem);
- binding.countries.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView> parent, View view, int position, long id) {
- loadHighScore(baseApplication, (Country) parent.getSelectedItem());
- }
-
- @Override
- public void onNothingSelected(AdapterView> parent) {
- }
- });
- }
-
- private void loadHighScore(BaseApplication baseApplication, Country selectedCountry) {
- var rsapi = baseApplication.getRsapiClient();
- var highScoreCall = selectedCountry.getCode().isEmpty() ? rsapi.getHighScore() : rsapi.getHighScore(selectedCountry.getCode());
- highScoreCall.enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- if (response.isSuccessful()) {
- adapter = new HighScoreAdapter(HighScoreActivity.this, response.body().getItems());
- binding.highscoreList.setAdapter(adapter);
- binding.highscoreList.setOnItemClickListener((adapter, v, position, arg3) -> {
- var highScoreItem = (HighScoreItem) adapter.getItemAtPosition(position);
- var stationFilter = baseApplication.getStationFilter();
- stationFilter.setNickname(highScoreItem.getName());
- baseApplication.setStationFilter(stationFilter);
- var intent = new Intent(HighScoreActivity.this, MapsActivity.class);
- startActivity(intent);
- });
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- Log.e(TAG, "Error loading highscore", t);
- Toast.makeText(getBaseContext(), getString(R.string.error_loading_highscore) + t.getMessage(), Toast.LENGTH_LONG).show();
- }
- });
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.menu_high_score, menu);
-
- var manager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
- var search = (SearchView) menu.findItem(R.id.search).getActionView();
- search.setSearchableInfo(manager.getSearchableInfo(getComponentName()));
- search.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
-
- @Override
- public boolean onQueryTextSubmit(String s) {
- Log.d(TAG, "onQueryTextSubmit ");
- if (adapter != null) {
- adapter.getFilter().filter(s);
- if (adapter.isEmpty()) {
- Toast.makeText(HighScoreActivity.this, R.string.no_records_found, Toast.LENGTH_LONG).show();
- } else {
- Toast.makeText(HighScoreActivity.this, getResources().getQuantityString(R.plurals.records_found, adapter.getCount(), adapter.getCount()), Toast.LENGTH_LONG).show();
- }
- }
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(String s) {
- Log.d(TAG, "onQueryTextChange ");
- if (adapter != null) {
- adapter.getFilter().filter(s);
- }
- return false;
- }
-
- });
-
- return true;
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/InboxActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/InboxActivity.java
deleted file mode 100644
index 47b62c03..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/InboxActivity.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-
-import java.util.List;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityInboxBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.db.InboxAdapter;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PublicInbox;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-public class InboxActivity extends AppCompatActivity {
-
- private static final String TAG = InboxActivity.class.getSimpleName();
-
- private InboxAdapter adapter;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- var binding = ActivityInboxBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- Call> inboxCall = ((BaseApplication)getApplication()).getRsapiClient().getPublicInbox();
- inboxCall.enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call> call, @NonNull Response> response) {
- var body = response.body();
- if (response.isSuccessful() && body != null) {
- adapter = new InboxAdapter(InboxActivity.this, body);
- binding.inboxList.setAdapter(adapter);
- binding.inboxList.setOnItemClickListener((parent, view, position, id) -> {
- var inboxItem = body.get(position);
- var intent = new Intent(InboxActivity.this, MapsActivity.class);
- intent.putExtra(MapsActivity.EXTRAS_LATITUDE, inboxItem.getLat());
- intent.putExtra(MapsActivity.EXTRAS_LONGITUDE, inboxItem.getLon());
- intent.putExtra(MapsActivity.EXTRAS_MARKER, inboxItem.getStationId() == null ? R.drawable.marker_missing : R.drawable.marker_red);
- startActivity(intent);
- });
- }
- }
-
- @Override
- public void onFailure(@NonNull Call> call, @NonNull Throwable t) {
- Log.e(TAG, "Error loading public inbox", t);
- Toast.makeText(getBaseContext(), getString(R.string.error_loading_inbox) + t.getMessage(), Toast.LENGTH_LONG).show();
- }
- });
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/IntroSliderActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/IntroSliderActivity.java
deleted file mode 100644
index 08ab5855..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/IntroSliderActivity.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.text.Html;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.viewpager.widget.PagerAdapter;
-import androidx.viewpager.widget.ViewPager;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityIntroSliderBinding;
-
-public class IntroSliderActivity extends AppCompatActivity {
-
- private ActivityIntroSliderBinding binding;
- private int[] layouts;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- BaseApplication baseApplication = (BaseApplication) getApplication();
-
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
-
- binding = ActivityIntroSliderBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- layouts = new int[]{R.layout.intro_slider1, R.layout.intro_slider2};
-
- addBottomDots(0);
- changeStatusBarColor();
- var viewPagerAdapter = new ViewPagerAdapter();
- binding.viewPager.setAdapter(viewPagerAdapter);
- binding.viewPager.addOnPageChangeListener(viewListener);
-
- binding.btnSliderSkip.setOnClickListener(v -> {
- baseApplication.setFirstAppStart(true);
- openMainActivity();
- });
-
- binding.btnSliderNext.setOnClickListener(v -> {
- int current = getNextItem();
- if (current < layouts.length) {
- binding.viewPager.setCurrentItem(current);
- } else {
- openMainActivity();
- }
- });
- }
-
- private void openMainActivity() {
- var intent = new Intent(IntroSliderActivity.this, MainActivity.class);
- startActivity(intent);
- finish();
- }
-
- @Override
- public void onBackPressed() {
- openMainActivity();
- }
-
- private void addBottomDots(int position) {
- var dots = new TextView[layouts.length];
- var colorActive = getResources().getIntArray(R.array.dot_active);
- var colorInactive = getResources().getIntArray(R.array.dot_inactive);
- binding.layoutDots.removeAllViews();
-
- for (int i = 0; i < dots.length; i++) {
- dots[i] = new TextView(this);
- dots[i].setText(Html.fromHtml("•", Html.FROM_HTML_MODE_LEGACY));
- dots[i].setTextSize(35);
- dots[i].setTextColor(colorInactive[position]);
- binding.layoutDots.addView(dots[i]);
- }
-
- if (dots.length > 0) {
- dots[position].setTextColor(colorActive[position]);
- }
- }
-
- private int getNextItem() {
- return binding.viewPager.getCurrentItem() + 1;
- }
-
- final ViewPager.OnPageChangeListener viewListener = new ViewPager.OnPageChangeListener() {
-
- @Override
- public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
-
- }
-
- @Override
- public void onPageSelected(int position) {
- var baseApplication = (BaseApplication) getApplication();
- addBottomDots(position);
-
- if (position == layouts.length - 1) {
- binding.btnSliderNext.setText(R.string.proceed);
- binding.btnSliderSkip.setVisibility(View.INVISIBLE);
- baseApplication.setFirstAppStart(true);
- } else {
- binding.btnSliderNext.setText(R.string.next);
- binding.btnSliderSkip.setVisibility(View.VISIBLE);
- }
- }
-
- @Override
- public void onPageScrollStateChanged(int state) {
-
- }
- };
-
- private void changeStatusBarColor() {
- var window = getWindow();
- window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- window.setStatusBarColor(Color.TRANSPARENT);
- }
-
- public class ViewPagerAdapter extends PagerAdapter {
-
- @Override
- @NonNull
- public Object instantiateItem(@NonNull ViewGroup container, int position) {
- var layoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- var view = layoutInflater.inflate(layouts[position], container, false);
- container.addView(view);
- return view;
- }
-
- @Override
- public void destroyItem(ViewGroup container, int position, @NonNull Object object) {
- var view = (View) object;
- container.removeView(view);
- }
-
- @Override
- public int getCount() {
- return layouts.length;
- }
-
- @Override
- public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
- return view == object;
- }
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/MainActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/MainActivity.java
deleted file mode 100644
index b822ae45..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/MainActivity.java
+++ /dev/null
@@ -1,500 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import android.Manifest;
-import android.app.AlertDialog;
-import android.app.SearchManager;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.pm.PackageManager;
-import android.location.Location;
-import android.location.LocationListener;
-import android.location.LocationManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.util.Log;
-import android.view.ContextThemeWrapper;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.ActionBarDrawerToggle;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.SearchView;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import androidx.core.view.GravityCompat;
-
-import com.google.android.material.navigation.NavigationView;
-
-import java.text.SimpleDateFormat;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityMainBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.db.DbAdapter;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.db.StationListAdapter;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.AppInfoFragment;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.StationFilterBar;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Statistic;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.UpdatePolicy;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi.RSAPIClient;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.StationFilter;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-public class MainActivity extends AppCompatActivity implements LocationListener, NavigationView.OnNavigationItemSelectedListener, StationFilterBar.OnChangeListener {
-
- private static final String DIALOG_TAG = "App Info Dialog";
- private static final long CHECK_UPDATE_INTERVAL = 10 * 60 * 1000; // 10 minutes
- private static final String TAG = MainActivity.class.getSimpleName();
- private static final int REQUEST_FINE_LOCATION = 1;
-
- // The minimum distance to change Updates in meters
- private static final long MIN_DISTANCE_CHANGE_FOR_UPDATES = 1000;
- // The minimum time between updates in milliseconds
- private static final long MIN_TIME_BW_UPDATES = 500; // minute
-
- private BaseApplication baseApplication;
- private DbAdapter dbAdapter;
-
- private ActivityMainBinding binding;
-
- private StationListAdapter stationListAdapter;
- private String searchString;
-
- private NearbyNotificationService.StatusBinder statusBinder;
- private RSAPIClient rsapiClient;
- private Location myPos;
- private LocationManager locationManager;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- binding = ActivityMainBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
- setSupportActionBar(binding.appBarMain.toolbar);
-
- baseApplication = (BaseApplication) getApplication();
- dbAdapter = baseApplication.getDbAdapter();
- rsapiClient = baseApplication.getRsapiClient();
-
- var toggle = new ActionBarDrawerToggle(
- this, binding.drawerLayout, binding.appBarMain.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
- binding.drawerLayout.addDrawerListener(toggle);
- toggle.syncState();
-
- binding.navView.setNavigationItemSelectedListener(this);
-
- var header = binding.navView.getHeaderView(0);
- TextView tvUpdate = header.findViewById(R.id.tvUpdate);
-
- if (!baseApplication.getFirstAppStart()) {
- startActivity(new Intent(this, IntroSliderActivity.class));
- finish();
- }
-
- var lastUpdateDate = baseApplication.getLastUpdate();
- if (lastUpdateDate > 0) {
- tvUpdate.setText(getString(R.string.last_update_at, SimpleDateFormat.getDateTimeInstance().format(lastUpdateDate)));
- } else {
- tvUpdate.setText(R.string.no_stations_in_database);
- }
-
- var searchIntent = getIntent();
- if (Intent.ACTION_SEARCH.equals(searchIntent.getAction())) {
- searchString = searchIntent.getStringExtra(SearchManager.QUERY);
- }
-
- myPos = baseApplication.getLastLocation();
- bindToStatus();
-
- binding.appBarMain.main.pullToRefresh.setOnRefreshListener(() -> {
- runUpdateCountriesAndStations();
- binding.appBarMain.main.pullToRefresh.setRefreshing(false);
- });
- }
-
- @Override
- public void onBackPressed() {
- if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
- binding.drawerLayout.closeDrawer(GravityCompat.START);
- } else {
- super.onBackPressed();
- }
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.main, menu);
-
- var manager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
- var searchMenu = menu.findItem(R.id.search);
- var search = (SearchView) searchMenu.getActionView();
- search.setSearchableInfo(manager.getSearchableInfo(getComponentName()));
- search.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
-
- @Override
- public boolean onQueryTextSubmit(String s) {
- Log.d(TAG, "onQueryTextSubmit: " + s);
- searchString = s;
- updateStationList();
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(String s) {
- Log.d(TAG, "onQueryTextChange: " + s);
- searchString = s;
- updateStationList();
- return false;
- }
-
- });
-
- var updatePolicy = baseApplication.getUpdatePolicy();
- menu.findItem(updatePolicy.getId()).setChecked(true);
-
- return true;
- }
-
- private void updateStationList() {
- try {
- var sortByDistance = baseApplication.getSortByDistance() && myPos != null;
- var stationCount = dbAdapter.countStations(baseApplication.getCountryCodes());
- var cursor = dbAdapter.getStationsListByKeyword(searchString, baseApplication.getStationFilter(), baseApplication.getCountryCodes(), sortByDistance, myPos);
- if (stationListAdapter != null) {
- stationListAdapter.swapCursor(cursor);
- } else {
- stationListAdapter = new StationListAdapter(this, cursor, 0);
- binding.appBarMain.main.lstStations.setAdapter(stationListAdapter);
-
- binding.appBarMain.main.lstStations.setOnItemClickListener((listview, view, position, id) -> {
- var intentDetails = new Intent(MainActivity.this, DetailsActivity.class);
- intentDetails.putExtra(DetailsActivity.EXTRA_STATION, dbAdapter.fetchStationByRowId(id));
- startActivity(intentDetails);
- });
- }
- binding.appBarMain.main.filterResult.setText(getString(R.string.filter_result, stationListAdapter.getCount(), stationCount));
- } catch (Exception e) {
- Log.e(TAG, "Unhandled Exception in onQueryTextSubmit", e);
- }
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- int id = item.getItemId();
-
- // necessary for the update policy submenu
- item.setChecked(!item.isChecked());
-
- if (id == R.id.rb_update_manual) {
- baseApplication.setUpdatePolicy(UpdatePolicy.MANUAL);
- } else if (id == R.id.rb_update_automatic) {
- baseApplication.setUpdatePolicy(UpdatePolicy.AUTOMATIC);
- } else if (id == R.id.rb_update_notify) {
- baseApplication.setUpdatePolicy(UpdatePolicy.NOTIFY);
- } else if (id == R.id.apiUrl) {
- showApiUrlDialog();
- }
-
- return super.onOptionsItemSelected(item);
- }
-
- private void showApiUrlDialog() {
- SimpleDialogs.prompt(this, R.string.apiUrl, EditorInfo.TYPE_TEXT_VARIATION_URI, R.string.api_url_hint, baseApplication.getApiUrl(), v -> {
- baseApplication.setApiUrl(v);
- baseApplication.setLastUpdate(0);
- recreate();
- });
- }
-
- private void setNotificationIcon(boolean active) {
- var item = binding.navView.getMenu().findItem(R.id.nav_notification);
- item.setIcon(ContextCompat.getDrawable(this, active ? R.drawable.ic_notifications_active_gray_24px : R.drawable.ic_notifications_off_gray_24px));
- }
-
- @Override
- public boolean onNavigationItemSelected(MenuItem item) {
- // Handle navigation view item clicks here.
- int id = item.getItemId();
- if (id == R.id.nav_slideshow) {
- startActivity(new Intent(this, IntroSliderActivity.class));
- finish();
- } else if (id == R.id.nav_your_data) {
- startActivity(new Intent(this, MyDataActivity.class));
- } else if (id == R.id.nav_update_photos) {
- runUpdateCountriesAndStations();
- } else if (id == R.id.nav_notification) {
- toggleNotification();
- } else if (id == R.id.nav_highscore) {
- startActivity(new Intent(this, HighScoreActivity.class));
- } else if (id == R.id.nav_outbox) {
- startActivity(new Intent(this, OutboxActivity.class));
- } else if (id == R.id.nav_inbox) {
- startActivity(new Intent(this, InboxActivity.class));
- } else if (id == R.id.nav_stations_map) {
- startActivity(new Intent(this, MapsActivity.class));
- } else if (id == R.id.nav_web_site) {
- startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://railway-stations.org")));
- } else if (id == R.id.nav_email) {
- var emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:" + getString(R.string.fab_email)));
- emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.fab_subject));
- startActivity(Intent.createChooser(emailIntent, getString(R.string.fab_chooser_title)));
- } else if (id == R.id.nav_app_info) {
- new AppInfoFragment().show(getSupportFragmentManager(), DIALOG_TAG);
- }
-
- binding.drawerLayout.closeDrawer(GravityCompat.START);
- return true;
- }
-
- private void runUpdateCountriesAndStations() {
- binding.appBarMain.main.progressBar.setVisibility(View.VISIBLE);
-
- rsapiClient.runUpdateCountriesAndStations(this, baseApplication, success -> {
- if (success) {
- TextView tvUpdate = findViewById(R.id.tvUpdate);
- tvUpdate.setText(getString(R.string.last_update_at, SimpleDateFormat.getDateTimeInstance().format(baseApplication.getLastUpdate())));
- updateStationList();
- }
- binding.appBarMain.main.progressBar.setVisibility(View.GONE);
- });
-
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- unregisterLocationManager();
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- for (int i = 0; i < binding.navView.getMenu().size(); i++) {
- binding.navView.getMenu().getItem(i).setChecked(false);
- }
-
- if (baseApplication.getLastUpdate() == 0) {
- runUpdateCountriesAndStations();
- } else if (System.currentTimeMillis() - baseApplication.getLastUpdate() > CHECK_UPDATE_INTERVAL) {
- baseApplication.setLastUpdate(System.currentTimeMillis());
- if (baseApplication.getUpdatePolicy() != UpdatePolicy.MANUAL) {
- for (var country : baseApplication.getCountryCodes()) {
- rsapiClient.getStatistic(country).enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- if (response.isSuccessful()) {
- checkForUpdates(response.body(), country);
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- Log.e(TAG, "Error loading country statistic", t);
- }
- });
- }
- }
- }
-
- if (baseApplication.getSortByDistance()) {
- registerLocationManager();
- }
-
- binding.appBarMain.main.stationFilterBar.init(baseApplication, this);
- updateStationList();
- }
-
- private void checkForUpdates(Statistic statistic, String country) {
- if (statistic == null) {
- return;
- }
-
- var dbStat = dbAdapter.getStatistic(country);
- Log.d(TAG, "DbStat: " + dbStat);
- if (statistic.getTotal() != dbStat.getTotal() || statistic.getWithPhoto() != dbStat.getWithPhoto() || statistic.getWithoutPhoto() != dbStat.getWithoutPhoto()) {
- if (baseApplication.getUpdatePolicy() == UpdatePolicy.AUTOMATIC) {
- runUpdateCountriesAndStations();
- } else {
- new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AlertDialogCustom))
- .setIcon(R.mipmap.ic_launcher)
- .setTitle(R.string.app_name)
- .setMessage(R.string.update_available)
- .setCancelable(true)
- .setPositiveButton(R.string.button_ok_text, (dialog, which) -> {
- runUpdateCountriesAndStations();
- dialog.dismiss();
- })
- .setNegativeButton(R.string.button_cancel_text, (dialog, which) -> dialog.dismiss())
- .create().show();
- }
- }
- }
-
- private void bindToStatus() {
- var intent = new Intent(this, NearbyNotificationService.class);
- intent.setAction(NearbyNotificationService.STATUS_INTERFACE);
- if (!this.getApplicationContext().bindService(intent, new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- Log.d(TAG, "Bound to status service of NearbyNotificationService");
- statusBinder = (NearbyNotificationService.StatusBinder) service;
- invalidateOptionsMenu();
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- Log.d(TAG, "Unbound from status service of NearbyNotificationService");
- statusBinder = null;
- invalidateOptionsMenu();
- }
- }, 0)) {
- Log.e(TAG, "Bind request to statistics interface failed");
- }
- }
-
- private final ActivityResultLauncher requestNotificationPermissionLauncher =
- registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
- if (!isGranted) {
- Toast.makeText(MainActivity.this, R.string.notification_permission_needed, Toast.LENGTH_SHORT).show();
- }
- });
-
- public void toggleNotification() {
- if (statusBinder == null
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
- && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
- requestNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
- return;
- }
-
- toggleNotificationWithPermissionGranted();
- }
-
- private void toggleNotificationWithPermissionGranted() {
- var intent = new Intent(MainActivity.this, NearbyNotificationService.class);
- if (statusBinder == null) {
- startService(intent);
- bindToStatus();
- setNotificationIcon(true);
- } else {
- stopService(intent);
- setNotificationIcon(false);
- }
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (requestCode == REQUEST_FINE_LOCATION) {
- Log.i(TAG, "Received response for location permission request.");
-
- // Check if the required permission has been granted
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- // Location permission has been granted
- registerLocationManager();
- } else {
- //Permission not granted
- baseApplication.setSortByDistance(false);
- binding.appBarMain.main.stationFilterBar.setSortOrder(false);
- }
- }
- }
-
- public void registerLocationManager() {
- try {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
- && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_FINE_LOCATION);
- return;
- }
-
- locationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
-
- // getting GPS status
- var isGPSEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
-
- // if GPS Enabled get lat/long using GPS Services
- if (isGPSEnabled) {
- locationManager.requestLocationUpdates(
- LocationManager.GPS_PROVIDER,
- MIN_TIME_BW_UPDATES,
- MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
- Log.d(TAG, "GPS Enabled");
- if (locationManager != null) {
- myPos = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
- }
- } else {
- // getting network status
- var isNetworkEnabled = locationManager
- .isProviderEnabled(LocationManager.NETWORK_PROVIDER);
-
- // First get location from Network Provider
- if (isNetworkEnabled) {
- locationManager.requestLocationUpdates(
- LocationManager.NETWORK_PROVIDER,
- MIN_TIME_BW_UPDATES,
- MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
- Log.d(TAG, "Network Location enabled");
- if (locationManager != null) {
- myPos = locationManager
- .getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
- }
- }
- }
- } catch (Exception e) {
- Log.e(TAG, "Error registering LocationManager", e);
- var b = new Bundle();
- b.putString("error", "Error registering LocationManager: " + e);
- locationManager = null;
- baseApplication.setSortByDistance(false);
- binding.appBarMain.main.stationFilterBar.setSortOrder(false);
- return;
- }
- Log.i(TAG, "LocationManager registered");
- onLocationChanged(myPos);
- }
-
- private void unregisterLocationManager() {
- if (locationManager != null) {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
- locationManager.removeUpdates(this);
- }
- locationManager = null;
- }
- Log.i(TAG, "LocationManager unregistered");
- }
-
- @Override
- public void onLocationChanged(@NonNull Location location) {
- myPos = location;
- updateStationList();
- }
-
- @Override
- public void stationFilterChanged(StationFilter stationFilter) {
- baseApplication.setStationFilter(stationFilter);
- updateStationList();
- }
-
- @Override
- public void sortOrderChanged(boolean sortByDistance) {
- if (sortByDistance) {
- registerLocationManager();
- }
- updateStationList();
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/MapsActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/MapsActivity.java
deleted file mode 100644
index 1354917f..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/MapsActivity.java
+++ /dev/null
@@ -1,1012 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import static android.view.Menu.NONE;
-
-import android.Manifest;
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.graphics.Color;
-import android.location.Location;
-import android.location.LocationListener;
-import android.location.LocationManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.WindowManager;
-import android.widget.CheckBox;
-import android.widget.Toast;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import androidx.core.content.res.ResourcesCompat;
-import androidx.documentfile.provider.DocumentFile;
-
-import org.mapsforge.core.graphics.Align;
-import org.mapsforge.core.graphics.Bitmap;
-import org.mapsforge.core.graphics.FontFamily;
-import org.mapsforge.core.graphics.FontStyle;
-import org.mapsforge.core.graphics.Style;
-import org.mapsforge.core.model.LatLong;
-import org.mapsforge.core.model.MapPosition;
-import org.mapsforge.core.model.Point;
-import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
-import org.mapsforge.map.android.input.MapZoomControls;
-import org.mapsforge.map.android.util.AndroidUtil;
-import org.mapsforge.map.datastore.MapDataStore;
-import org.mapsforge.map.layer.Layer;
-import org.mapsforge.map.layer.cache.TileCache;
-import org.mapsforge.map.layer.download.TileDownloadLayer;
-import org.mapsforge.map.layer.download.tilesource.AbstractTileSource;
-import org.mapsforge.map.layer.download.tilesource.OnlineTileSource;
-import org.mapsforge.map.layer.download.tilesource.OpenStreetMapMapnik;
-import org.mapsforge.map.layer.overlay.Marker;
-import org.mapsforge.map.layer.renderer.TileRendererLayer;
-import org.mapsforge.map.model.IMapViewPosition;
-import org.mapsforge.map.reader.MapFile;
-import org.mapsforge.map.rendertheme.InternalRenderTheme;
-import org.mapsforge.map.rendertheme.StreamRenderTheme;
-import org.mapsforge.map.rendertheme.XmlRenderTheme;
-
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityMapsBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.db.DbAdapter;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.MapInfoFragment;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.StationFilterBar;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge.ClusterManager;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge.DbsTileSource;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge.GeoItem;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge.MarkerBitmap;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge.TapHandler;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Upload;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.StationFilter;
-
-public class MapsActivity extends AppCompatActivity implements LocationListener, TapHandler, StationFilterBar.OnChangeListener {
-
- public static final String EXTRAS_LATITUDE = "Extras_Latitude";
- public static final String EXTRAS_LONGITUDE = "Extras_Longitude";
- public static final String EXTRAS_MARKER = "Extras_Marker";
-
- // The minimum distance to change Updates in meters
- private static final long MIN_DISTANCE_CHANGE_FOR_UPDATES = 1; // meters
-
- // The minimum time between updates in milliseconds
- private static final long MIN_TIME_BW_UPDATES = 500; // minute
-
- private static final String TAG = MapsActivity.class.getSimpleName();
- private static final int REQUEST_FINE_LOCATION = 1;
- private static final String USER_AGENT = "railway-stations.org-android";
-
- private final Map onlineTileSources = new HashMap<>();
- protected Layer layer;
- protected ClusterManager clusterer = null;
- protected final List tileCaches = new ArrayList<>();
- private LatLong myPos = null;
- private CheckBox myLocSwitch = null;
- private DbAdapter dbAdapter;
- private String nickname;
- private BaseApplication baseApplication;
- private LocationManager locationManager;
- private boolean askedForPermission = false;
- private Marker missingMarker;
-
- private ActivityMapsBinding binding;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- AndroidGraphicFactory.createInstance(this.getApplication());
-
- binding = ActivityMapsBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
- var window = getWindow();
- window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- window.setStatusBarColor(Color.parseColor("#c71c4d"));
-
- setSupportActionBar(binding.mapsToolbar);
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-
- baseApplication = (BaseApplication) getApplication();
- dbAdapter = baseApplication.getDbAdapter();
- nickname = baseApplication.getNickname();
-
- var intent = getIntent();
- Marker extraMarker = null;
- if (intent != null) {
- var latitude = (Double) intent.getSerializableExtra(EXTRAS_LATITUDE);
- var longitude = (Double) intent.getSerializableExtra(EXTRAS_LONGITUDE);
- setMyLocSwitch(false);
- if (latitude != null && longitude != null) {
- myPos = new LatLong(latitude, longitude);
- }
-
- var markerRes = (Integer) intent.getSerializableExtra(EXTRAS_MARKER);
- if (markerRes != null) {
- extraMarker = createBitmapMarker(myPos, markerRes);
- }
- }
-
- addDBSTileSource(R.string.dbs_osm_basic, "/styles/dbs-osm-basic/");
- addDBSTileSource(R.string.dbs_osm_railway, "/styles/dbs-osm-railway/");
-
- createMapViews();
- createTileCaches();
- checkPermissionsAndCreateLayersAndControls();
-
- if (extraMarker != null) {
- binding.map.mapView.getLayerManager().getLayers().add(extraMarker);
- }
- }
-
- private void addDBSTileSource(int nameResId, String baseUrl) {
- var dbsBasic = new DbsTileSource(getString(nameResId), baseUrl);
- onlineTileSources.put(dbsBasic.getName(), dbsBasic);
- }
-
- protected void createTileCaches() {
- this.tileCaches.add(AndroidUtil.createTileCache(this, getPersistableId(),
- this.binding.map.mapView.getModel().displayModel.getTileSize(), this.getScreenRatio(),
- this.binding.map.mapView.getModel().frameBufferModel.getOverdrawFactor(), true));
- }
-
- /**
- * The persistable ID is used to store settings information, like the center of the last view
- * and the zoomlevel. By default the simple name of the class is used. The value is not user
- * visibile.
- *
- * @return the id that is used to save this mapview.
- */
- protected String getPersistableId() {
- return this.getClass().getSimpleName();
- }
-
- /**
- * Returns the relative size of a map view in relation to the screen size of the device. This
- * is used for cache size calculations.
- * By default this returns 1.0, for a full size map view.
- *
- * @return the screen ratio of the mapview
- */
- protected float getScreenRatio() {
- return 1.0f;
- }
-
- /**
- * Hook to check for Android Runtime Permissions.
- */
- protected void checkPermissionsAndCreateLayersAndControls() {
- createLayers();
- createControls();
- }
-
- /**
- * Hook to create controls, such as scale bars.
- * You can add more controls.
- */
- protected void createControls() {
- initializePosition(binding.map.mapView.getModel().mapViewPosition);
- }
-
- /**
- * initializes the map view position.
- *
- * @param mvp the map view position to be set
- */
- protected void initializePosition(IMapViewPosition mvp) {
- if (myPos != null) {
- mvp.setMapPosition(new MapPosition(myPos, baseApplication.getZoomLevelDefault()));
- } else {
- mvp.setMapPosition(baseApplication.getLastMapPosition());
- }
- mvp.setZoomLevelMax(getZoomLevelMax());
- mvp.setZoomLevelMin(getZoomLevelMin());
- }
-
- /**
- * Template method to create the map views.
- */
- protected void createMapViews() {
- binding.map.mapView.setClickable(true);
- binding.map.mapView.setOnMapDragListener(() -> myLocSwitch.setChecked(false));
- binding.map.mapView.getMapScaleBar().setVisible(true);
- binding.map.mapView.setBuiltInZoomControls(true);
- binding.map.mapView.getMapZoomControls().setAutoHide(true);
- binding.map.mapView.getMapZoomControls().setZoomLevelMin(getZoomLevelMin());
- binding.map.mapView.getMapZoomControls().setZoomLevelMax(getZoomLevelMax());
-
- binding.map.mapView.getMapZoomControls().setZoomControlsOrientation(MapZoomControls.Orientation.VERTICAL_IN_OUT);
- binding.map.mapView.getMapZoomControls().setZoomInResource(R.drawable.zoom_control_in);
- binding.map.mapView.getMapZoomControls().setZoomOutResource(R.drawable.zoom_control_out);
- binding.map.mapView.getMapZoomControls().setMarginHorizontal(getResources().getDimensionPixelOffset(R.dimen.controls_margin));
- binding.map.mapView.getMapZoomControls().setMarginVertical(getResources().getDimensionPixelOffset(R.dimen.controls_margin));
- }
-
- protected byte getZoomLevelMax() {
- return binding.map.mapView.getModel().mapViewPosition.getZoomLevelMax();
- }
-
- protected byte getZoomLevelMin() {
- return binding.map.mapView.getModel().mapViewPosition.getZoomLevelMin();
- }
-
- /**
- * Hook to purge tile caches.
- * By default we purge every tile cache that has been added to the tileCaches list.
- */
- protected void purgeTileCaches() {
- for (TileCache tileCache : tileCaches) {
- tileCache.purge();
- }
- tileCaches.clear();
- }
-
- protected XmlRenderTheme getRenderTheme() {
- var mapTheme = baseApplication.getMapThemeUri();
- return mapTheme.map(m -> {
- try {
- var renderThemeFile = DocumentFile.fromSingleUri(getApplication(), m);
- return new StreamRenderTheme("/assets/", getContentResolver().openInputStream(renderThemeFile.getUri()));
- } catch (Exception e) {
- Log.e(TAG, "Error loading theme " + mapTheme, e);
- return InternalRenderTheme.DEFAULT;
- }
- }).orElse(InternalRenderTheme.DEFAULT);
- }
-
- protected Optional getMapFile() {
- var mapUri = BaseApplication.toUri(baseApplication.getMap());
- return mapUri.map(map -> {
- if (!DocumentFile.isDocumentUri(this, map)) {
- return null;
- }
- try {
- var inputStream = (FileInputStream) getContentResolver().openInputStream(map);
- return new MapFile(inputStream, 0, null);
- } catch (FileNotFoundException e) {
- Log.e(TAG, "Can't open mapFile", e);
- }
- return null;
- });
- }
-
- protected void createLayers() {
- var mapFile = getMapFile();
-
- if (mapFile.isPresent()) {
- var rendererLayer = new TileRendererLayer(this.tileCaches.get(0), mapFile.get(),
- this.binding.map.mapView.getModel().mapViewPosition, false, true, false, AndroidGraphicFactory.INSTANCE) {
- @Override
- public boolean onLongPress(LatLong tapLatLong, Point thisXY,
- Point tapXY) {
- MapsActivity.this.onLongPress(tapLatLong);
- return true;
- }
- };
- rendererLayer.setXmlRenderTheme(getRenderTheme());
- this.layer = rendererLayer;
- binding.map.mapView.getLayerManager().getLayers().add(this.layer);
- } else {
- AbstractTileSource tileSource = onlineTileSources.get(baseApplication.getMap());
-
- if (tileSource == null) {
- tileSource = OpenStreetMapMapnik.INSTANCE;
- }
-
- tileSource.setUserAgent(USER_AGENT);
- this.layer = new TileDownloadLayer(this.tileCaches.get(0),
- this.binding.map.mapView.getModel().mapViewPosition, tileSource,
- AndroidGraphicFactory.INSTANCE) {
- @Override
- public boolean onLongPress(LatLong tapLatLong, Point thisXY,
- Point tapXY) {
- MapsActivity.this.onLongPress(tapLatLong);
- return true;
- }
- };
- binding.map.mapView.getLayerManager().getLayers().add(this.layer);
-
- binding.map.mapView.setZoomLevelMin(tileSource.getZoomLevelMin());
- binding.map.mapView.setZoomLevelMax(tileSource.getZoomLevelMax());
- }
-
- }
-
- private Marker createBitmapMarker(LatLong latLong, int markerRes) {
- var drawable = ContextCompat.getDrawable(this, markerRes);
- assert drawable != null;
- var bitmap = AndroidGraphicFactory.convertToBitmap(drawable);
- return new Marker(latLong, bitmap, -(bitmap.getWidth() / 2), -bitmap.getHeight());
- }
-
- private void onLongPress(LatLong tapLatLong) {
- if (missingMarker == null) {
- // marker to show at the location
- var drawable = ContextCompat.getDrawable(this, R.drawable.marker_missing);
- assert drawable != null;
- var bitmap = AndroidGraphicFactory.convertToBitmap(drawable);
- missingMarker = new Marker(tapLatLong, bitmap, -(bitmap.getWidth() / 2), -bitmap.getHeight()) {
- @Override
- public boolean onTap(LatLong tapLatLong, Point layerXY, Point tapXY) {
- SimpleDialogs.confirmOkCancel(MapsActivity.this, R.string.add_missing_station, (dialogInterface, i) -> {
- var intent = new Intent(MapsActivity.this, UploadActivity.class);
- intent.putExtra(UploadActivity.EXTRA_LATITUDE, getLatLong().latitude);
- intent.putExtra(UploadActivity.EXTRA_LONGITUDE, getLatLong().longitude);
- startActivity(intent);
- });
- return false;
- }
- };
- binding.map.mapView.getLayerManager().getLayers().add(missingMarker);
- } else {
- missingMarker.setLatLong(tapLatLong);
- missingMarker.requestRedraw();
- }
-
- // feedback for long click
- ((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE));
-
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- super.onCreateOptionsMenu(menu);
- getMenuInflater().inflate(R.menu.maps, menu);
-
- var item = menu.findItem(R.id.menu_toggle_mypos);
- myLocSwitch = new CheckBox(this);
- myLocSwitch.setButtonDrawable(R.drawable.ic_gps_fix_selector);
- myLocSwitch.setChecked(baseApplication.isLocationUpdates());
- item.setActionView(myLocSwitch);
- myLocSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
- baseApplication.setLocationUpdates(isChecked);
- if (isChecked) {
- askedForPermission = false;
- registerLocationManager();
- } else {
- unregisterLocationManager();
- }
- }
- );
-
- var map = baseApplication.getMap();
-
- var osmMapnick = menu.findItem(R.id.osm_mapnik);
- osmMapnick.setChecked(map == null);
- osmMapnick.setOnMenuItemClickListener(new MapMenuListener(this, baseApplication, null));
-
- var mapSubmenu = menu.findItem(R.id.maps_submenu).getSubMenu();
- assert mapSubmenu != null;
- for (var tileSource : onlineTileSources.values()) {
- var mapItem = mapSubmenu.add(R.id.maps_group, NONE, NONE, tileSource.getName());
- mapItem.setChecked(tileSource.getName().equals(map));
- mapItem.setOnMenuItemClickListener(new MapMenuListener(this, baseApplication, tileSource.getName()));
- }
-
- var mapDirectory = baseApplication.getMapDirectoryUri();
- if (mapDirectory.isPresent()) {
- var documentsTree = getDocumentFileFromTreeUri(mapDirectory.get());
- if (documentsTree != null) {
- for (var file : documentsTree.listFiles()) {
- if (file.isFile() && file.getName().endsWith(".map")) {
- var mapItem = mapSubmenu.add(R.id.maps_group, NONE, NONE, file.getName());
- mapItem.setChecked(BaseApplication.toUri(map).map(uri -> file.getUri().equals(uri)).orElse(false));
- mapItem.setOnMenuItemClickListener(new MapMenuListener(this, baseApplication, file.getUri().toString()));
- }
- }
- }
- }
- mapSubmenu.setGroupCheckable(R.id.maps_group, true, true);
-
- var mapFolder = mapSubmenu.add(R.string.map_folder);
- mapFolder.setOnMenuItemClickListener(item1 -> {
- openMapDirectoryChooser();
- return false;
- });
-
- var mapTheme = baseApplication.getMapThemeUri();
- var mapThemeDirectory = baseApplication.getMapThemeDirectoryUri();
-
- var defaultTheme = menu.findItem(R.id.default_theme);
- defaultTheme.setChecked(!mapTheme.isPresent());
- defaultTheme.setOnMenuItemClickListener(new MapThemeMenuListener(this, baseApplication, null));
- var themeSubmenu = menu.findItem(R.id.themes_submenu).getSubMenu();
- assert themeSubmenu != null;
-
- if (mapThemeDirectory.isPresent()) {
- var documentsTree = getDocumentFileFromTreeUri(mapThemeDirectory.get());
- if (documentsTree != null) {
- for (var file : documentsTree.listFiles()) {
- if (file.isFile() && file.getName().endsWith(".xml")) {
- var themeName = file.getName();
- var themeItem = themeSubmenu.add(R.id.themes_group, NONE, NONE, themeName);
- themeItem.setChecked(mapTheme.map(uri -> file.getUri().equals(uri)).orElse(false));
- themeItem.setOnMenuItemClickListener(new MapThemeMenuListener(this, baseApplication, file.getUri()));
- } else if (file.isDirectory()) {
- var childFile = file.findFile(file.getName() + ".xml");
- if (childFile != null) {
- var themeName = file.getName();
- var themeItem = themeSubmenu.add(R.id.themes_group, NONE, NONE, themeName);
- themeItem.setChecked(mapTheme.map(uri -> childFile.getUri().equals(uri)).orElse(false));
- themeItem.setOnMenuItemClickListener(new MapThemeMenuListener(this, baseApplication, childFile.getUri()));
- }
- }
- }
- }
- }
- themeSubmenu.setGroupCheckable(R.id.themes_group, true, true);
-
- var themeFolder = themeSubmenu.add(R.string.theme_folder);
- themeFolder.setOnMenuItemClickListener(item12 -> {
- openThemeDirectoryChooser();
- return false;
- });
-
- return true;
- }
-
- private DocumentFile getDocumentFileFromTreeUri(Uri uri) {
- try {
- return DocumentFile.fromTreeUri(getApplication(), uri);
- } catch (Exception e) {
- Log.w(TAG, "Error getting DocumentFile from Uri: " + uri);
- }
- return null;
- }
-
- protected ActivityResultLauncher themeDirectoryLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
- result -> {
- if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
- var uri = result.getData().getData();
- if (uri != null) {
- getContentResolver().takePersistableUriPermission(
- uri,
- Intent.FLAG_GRANT_READ_URI_PERMISSION
- );
- baseApplication.setMapThemeDirectoryUri(uri);
- recreate();
- }
- }
- });
-
- protected ActivityResultLauncher mapDirectoryLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
- result -> {
- if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
- var uri = result.getData().getData();
- if (uri != null) {
- getContentResolver().takePersistableUriPermission(
- uri,
- Intent.FLAG_GRANT_READ_URI_PERMISSION
- );
- baseApplication.setMapDirectoryUri(uri);
- recreate();
- }
- }
- });
-
- public void openDirectory(ActivityResultLauncher launcher) {
- var intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
- launcher.launch(intent);
- }
-
-
- private void openMapDirectoryChooser() {
- openDirectory(mapDirectoryLauncher);
- }
-
- private void openThemeDirectoryChooser() {
- openDirectory(themeDirectoryLauncher);
- }
-
- /**
- * Android Activity life cycle method.
- */
- @Override
- protected void onDestroy() {
- binding.map.mapView.destroyAll();
- AndroidGraphicFactory.clearResourceMemoryCache();
- purgeTileCaches();
- super.onDestroy();
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == R.id.map_info) {
- new MapInfoFragment().show(getSupportFragmentManager(), "Map Info Dialog");
- } else {
- return super.onOptionsItemSelected(item);
- }
- return true;
- }
-
- private void reloadMap() {
- destroyClusterManager();
- new LoadMapMarkerTask(this).start();
- }
-
- private void runUpdateCountriesAndStations() {
- binding.map.progressBar.setVisibility(View.VISIBLE);
- baseApplication.getRsapiClient().runUpdateCountriesAndStations(this, baseApplication, success -> reloadMap());
- }
-
-
- private void onStationsLoaded(List stationList, List uploadList) {
- try {
- createClusterManager();
- addMarkers(stationList, uploadList);
- binding.map.progressBar.setVisibility(View.GONE);
- Toast.makeText(this, getResources().getQuantityString(R.plurals.stations_loaded, stationList.size(), stationList.size()), Toast.LENGTH_LONG).show();
- } catch (Exception e) {
- Log.e(TAG, "Error loading markers", e);
- }
- }
-
- @Override
- public void onTap(BahnhofGeoItem marker) {
- var intent = new Intent(MapsActivity.this, DetailsActivity.class);
- var id = marker.getStation().getId();
- var country = marker.getStation().getCountry();
- try {
- var station = dbAdapter.getStationByKey(country, id);
- intent.putExtra(DetailsActivity.EXTRA_STATION, station);
- startActivity(intent);
- } catch (RuntimeException e) {
- Log.wtf(TAG, String.format("Could not fetch station id %s that we put onto the map", id), e);
- }
- }
-
- public void setMyLocSwitch(boolean checked) {
- if (myLocSwitch != null) {
- myLocSwitch.setChecked(checked);
- }
- baseApplication.setLocationUpdates(checked);
- }
-
- @Override
- public void stationFilterChanged(StationFilter stationFilter) {
- reloadMap();
- }
-
- @Override
- public void sortOrderChanged(boolean sortByDistance) {
- // unused
- }
-
- private static class LoadMapMarkerTask extends Thread {
- private final WeakReference activityRef;
-
- public LoadMapMarkerTask(MapsActivity activity) {
- this.activityRef = new WeakReference<>(activity);
- }
-
- @Override
- public void run() {
- var stationList = activityRef.get().readStations();
- var uploadList = activityRef.get().readPendingUploads();
- var mapsActivity = activityRef.get();
- if (mapsActivity != null) {
- mapsActivity.runOnUiThread(() -> mapsActivity.onStationsLoaded(stationList, uploadList));
- }
- }
-
- }
-
- private List readStations() {
- try {
- return dbAdapter.getAllStations(baseApplication.getStationFilter(), baseApplication.getCountryCodes());
- } catch (Exception e) {
- Log.i(TAG, "Datenbank konnte nicht geöffnet werden");
- }
- return null;
- }
-
- private List readPendingUploads() {
- try {
- return dbAdapter.getPendingUploads(false);
- } catch (Exception e) {
- Log.i(TAG, "Datenbank konnte nicht geöffnet werden");
- }
- return null;
- }
-
- private List createMarkerBitmaps() {
- var markerBitmaps = new ArrayList();
- markerBitmaps.add(createSmallSingleIconMarker());
- markerBitmaps.add(createSmallClusterIconMarker());
- markerBitmaps.add(createLargeClusterIconMarker());
- return markerBitmaps;
- }
-
- /**
- * large cluster icon. 100 will be ignored.
- */
- private MarkerBitmap createLargeClusterIconMarker() {
- var bitmapBalloonMN = loadBitmap(R.drawable.balloon_m_n);
- var paint = AndroidGraphicFactory.INSTANCE.createPaint();
- paint.setStyle(Style.FILL);
- paint.setTextAlign(Align.CENTER);
- paint.setTypeface(FontFamily.DEFAULT, FontStyle.BOLD);
- paint.setColor(Color.BLACK);
- return new MarkerBitmap(this.getApplicationContext(), bitmapBalloonMN,
- new Point(0, 0), 11f, 100, paint);
- }
-
- /**
- * small cluster icon. for 10 or less items.
- */
- private MarkerBitmap createSmallClusterIconMarker() {
- var bitmapBalloonSN = loadBitmap(R.drawable.balloon_s_n);
- var paint = AndroidGraphicFactory.INSTANCE.createPaint();
- paint.setStyle(Style.FILL);
- paint.setTextAlign(Align.CENTER);
- paint.setTypeface(FontFamily.DEFAULT, FontStyle.BOLD);
- paint.setColor(Color.BLACK);
- return new MarkerBitmap(this.getApplicationContext(), bitmapBalloonSN,
- new Point(0, 0), 9f, 10, paint);
- }
-
- private MarkerBitmap createSmallSingleIconMarker() {
- var bitmapWithPhoto = loadBitmap(R.drawable.marker_green);
- var markerWithoutPhoto = loadBitmap(R.drawable.marker_red);
- var markerOwnPhoto = loadBitmap(R.drawable.marker_violet);
- var markerPendingUpload = loadBitmap(R.drawable.marker_yellow);
-
- var markerWithPhotoInactive = loadBitmap(R.drawable.marker_green_inactive);
- var markerWithoutPhotoInactive = loadBitmap(R.drawable.marker_red_inactive);
- var markerOwnPhotoInactive = loadBitmap(R.drawable.marker_violet_inactive);
-
- var paint = AndroidGraphicFactory.INSTANCE.createPaint();
- paint.setStyle(Style.FILL);
- paint.setTextAlign(Align.CENTER);
- paint.setTypeface(FontFamily.DEFAULT, FontStyle.BOLD);
- paint.setColor(Color.RED);
-
- return new MarkerBitmap(this.getApplicationContext(), markerWithoutPhoto, bitmapWithPhoto, markerOwnPhoto,
- markerWithoutPhotoInactive, markerWithPhotoInactive, markerOwnPhotoInactive, markerPendingUpload,
- new Point(0, -(markerWithoutPhoto.getHeight() / 2.0)), 10f, 1, paint);
- }
-
- private Bitmap loadBitmap(int resourceId) {
- var bitmap = AndroidGraphicFactory.convertToBitmap(ResourcesCompat.getDrawable(getResources(), resourceId, null));
- bitmap.incrementRefCount();
- return bitmap;
- }
-
- private void addMarkers(List stationMarker, List uploadList) {
- double minLat = 0;
- double maxLat = 0;
- double minLon = 0;
- double maxLon = 0;
- for (var station : stationMarker) {
- var isPendingUpload = isPendingUpload(station, uploadList);
- var geoItem = new BahnhofGeoItem(station, isPendingUpload);
- var bahnhofPos = geoItem.getLatLong();
- if (minLat == 0.0) {
- minLat = bahnhofPos.latitude;
- maxLat = bahnhofPos.latitude;
- minLon = bahnhofPos.longitude;
- maxLon = bahnhofPos.longitude;
- } else {
- minLat = Math.min(minLat, bahnhofPos.latitude);
- maxLat = Math.max(maxLat, bahnhofPos.latitude);
- minLon = Math.min(minLon, bahnhofPos.longitude);
- maxLon = Math.max(maxLon, bahnhofPos.longitude);
- }
- clusterer.addItem(geoItem);
- }
-
- clusterer.redraw();
-
- if (myPos == null || (myPos.latitude == 0.0 && myPos.longitude == 0.0)) {
- myPos = new LatLong((minLat + maxLat) / 2, (minLon + maxLon) / 2);
- }
- updatePosition();
- }
-
- private boolean isPendingUpload(Station station, List uploadList) {
- for (var upload : uploadList) {
- if (upload.isPendingPhotoUpload() && station.getId().equals(upload.getStationId()) && station.getCountry().equals(upload.getCountry())) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if (this.layer instanceof TileDownloadLayer) {
- ((TileDownloadLayer) this.layer).onResume();
- }
- if (baseApplication.getLastUpdate() == 0) {
- runUpdateCountriesAndStations();
- } else {
- reloadMap();
- }
- if (baseApplication.isLocationUpdates()) {
- registerLocationManager();
- }
- binding.map.stationFilterBar.init(baseApplication, this);
- binding.map.stationFilterBar.setSortOrderEnabled(false);
- }
-
- private void createClusterManager() {
- // create clusterer instance
- clusterer = new ClusterManager<>(
- binding.map.mapView,
- createMarkerBitmaps(),
- (byte) 9,
- this);
- // this uses the framebuffer position, the mapview position can be out of sync with
- // what the user sees on the screen if an animation is in progress
- this.binding.map.mapView.getModel().frameBufferModel.addObserver(clusterer);
- }
-
- @Override
- protected void onPause() {
- if (this.layer instanceof TileDownloadLayer) {
- ((TileDownloadLayer) this.layer).onPause();
- }
- unregisterLocationManager();
- var mapPosition = binding.map.mapView.getModel().mapViewPosition.getMapPosition();
- baseApplication.setLastMapPosition(mapPosition);
- destroyClusterManager();
- super.onPause();
- }
-
- private void destroyClusterManager() {
- if (clusterer != null) {
- clusterer.destroyGeoClusterer();
- this.binding.map.mapView.getModel().frameBufferModel.removeObserver(clusterer);
- clusterer = null;
- }
- }
-
- @Override
- public void onLocationChanged(Location location) {
- myPos = new LatLong(location.getLatitude(), location.getLongitude());
- updatePosition();
- }
-
- @Override
- public void onStatusChanged(String provider, int status, Bundle extras) {
-
- }
-
- @Override
- public void onProviderEnabled(@NonNull String provider) {
-
- }
-
- @Override
- public void onProviderDisabled(@NonNull String provider) {
-
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (requestCode == REQUEST_FINE_LOCATION) {
- Log.i(TAG, "Received response for location permission request.");
-
- // Check if the required permission has been granted
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- // Location permission has been granted
- registerLocationManager();
- } else {
- //Permission not granted
- Toast.makeText(MapsActivity.this, R.string.grant_location_permission, Toast.LENGTH_LONG).show();
- }
- }
- }
-
- public void registerLocationManager() {
-
- try {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
- if (!askedForPermission) {
- ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_FINE_LOCATION);
- askedForPermission = true;
- }
- setMyLocSwitch(false);
- return;
- }
-
- locationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
-
- // getting GPS status
- var isGPSEnabled = locationManager
- .isProviderEnabled(LocationManager.GPS_PROVIDER);
-
- // if GPS Enabled get lat/long using GPS Services
- if (isGPSEnabled) {
- locationManager.requestLocationUpdates(
- LocationManager.GPS_PROVIDER,
- MIN_TIME_BW_UPDATES,
- MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
- Log.d(TAG, "GPS Enabled");
- if (locationManager != null) {
- var loc = locationManager
- .getLastKnownLocation(LocationManager.GPS_PROVIDER);
- myPos = new LatLong(loc.getLatitude(), loc.getLongitude());
- }
- } else {
- // getting network status
- var isNetworkEnabled = locationManager
- .isProviderEnabled(LocationManager.NETWORK_PROVIDER);
-
- // First get location from Network Provider
- if (isNetworkEnabled) {
- locationManager.requestLocationUpdates(
- LocationManager.NETWORK_PROVIDER,
- MIN_TIME_BW_UPDATES,
- MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
- Log.d(TAG, "Network Location enabled");
- if (locationManager != null) {
- var loc = locationManager
- .getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
- myPos = new LatLong(loc.getLatitude(), loc.getLongitude());
- }
- }
- }
- setMyLocSwitch(true);
- } catch (Exception e) {
- Log.e(TAG, "Error registering LocationManager", e);
- var b = new Bundle();
- b.putString("error", "Error registering LocationManager: " + e);
- locationManager = null;
- myPos = null;
- setMyLocSwitch(false);
- return;
- }
- Log.i(TAG, "LocationManager registered");
- updatePosition();
- }
-
- private void unregisterLocationManager() {
- if (locationManager != null) {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
- locationManager.removeUpdates(this);
- }
- locationManager = null;
- }
- Log.i(TAG, "LocationManager unregistered");
- }
-
- private void updatePosition() {
- if (myLocSwitch != null && myLocSwitch.isChecked()) {
- binding.map.mapView.setCenter(myPos);
- binding.map.mapView.repaint();
- }
- }
-
- protected class BahnhofGeoItem implements GeoItem {
- public Station station;
- public final LatLong latLong;
- public final boolean pendingUpload;
-
- public BahnhofGeoItem(Station station, boolean pendingUpload) {
- this.station = station;
- this.pendingUpload = pendingUpload;
- this.latLong = new LatLong(station.getLat(), station.getLon());
- }
-
- public LatLong getLatLong() {
- return latLong;
- }
-
- @Override
- public String getTitle() {
- return station.getTitle();
- }
-
- @Override
- public boolean hasPhoto() {
- return station.hasPhoto();
- }
-
- @Override
- public boolean ownPhoto() {
- return hasPhoto() && station.getPhotographer().equals(nickname);
- }
-
- @Override
- public boolean stationActive() {
- return station.getActive();
- }
-
- @Override
- public boolean isPendingUpload() {
- return pendingUpload;
- }
-
- public Station getStation() {
- return station;
- }
-
- }
-
- private static class MapMenuListener implements MenuItem.OnMenuItemClickListener {
-
- private final WeakReference mapsActivityRef;
-
- private final BaseApplication baseApplication;
-
- private final String map;
-
- private MapMenuListener(MapsActivity mapsActivity, BaseApplication baseApplication, String map) {
- this.mapsActivityRef = new WeakReference<>(mapsActivity);
- this.baseApplication = baseApplication;
- this.map = map;
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- item.setChecked(true);
- if (item.getItemId() == R.id.osm_mapnik) { // default Mapnik online tiles
- baseApplication.setMap(null);
- } else {
- baseApplication.setMap(map);
- }
-
- var mapsActivity = mapsActivityRef.get();
- if (mapsActivity != null) {
- mapsActivity.recreate();
- }
- return false;
- }
- }
-
- private static class MapThemeMenuListener implements MenuItem.OnMenuItemClickListener {
-
- private final WeakReference mapsActivityRef;
-
- private final BaseApplication baseApplication;
-
- private final Uri mapThemeUri;
-
- private MapThemeMenuListener(MapsActivity mapsActivity, BaseApplication baseApplication, Uri mapThemeUri) {
- this.mapsActivityRef = new WeakReference<>(mapsActivity);
- this.baseApplication = baseApplication;
- this.mapThemeUri = mapThemeUri;
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- item.setChecked(true);
- if (item.getItemId() == R.id.default_theme) { // default theme
- baseApplication.setMapThemeUri(null);
- } else {
- baseApplication.setMapThemeUri(mapThemeUri);
- }
-
- var mapsActivity = mapsActivityRef.get();
- if (mapsActivity != null) {
- mapsActivity.recreate();
- }
- return false;
- }
- }
-
-}
-
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/MyDataActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/MyDataActivity.java
deleted file mode 100644
index bcb89ae4..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/MyDataActivity.java
+++ /dev/null
@@ -1,452 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.ContextThemeWrapper;
-import android.view.View;
-import android.widget.EditText;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.UnsupportedEncodingException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.security.NoSuchAlgorithmException;
-import java.util.Objects;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityMydataBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ChangePasswordBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.License;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Profile;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Token;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi.RSAPIClient;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-public class MyDataActivity extends AppCompatActivity {
-
- private static final String TAG = MyDataActivity.class.getSimpleName();
-
- private License license;
- private BaseApplication baseApplication;
- private RSAPIClient rsapiClient;
- private Profile profile;
- private ActivityMydataBinding binding;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- binding = ActivityMydataBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
- Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
- getSupportActionBar().setTitle(R.string.login);
-
- binding.myData.profileForm.setVisibility(View.INVISIBLE);
-
- baseApplication = (BaseApplication) getApplication();
- rsapiClient = baseApplication.getRsapiClient();
-
- setProfileToUI(baseApplication.getProfile());
-
- oauthAuthorizationCallback(getIntent());
- if (isLoginDataAvailable()) {
- loadRemoteProfile();
- }
- }
-
- private void setProfileToUI(Profile profile) {
- binding.myData.etNickname.setText(profile.getNickname());
- binding.myData.etEmail.setText(profile.getEmail());
- binding.myData.etLinking.setText(profile.getLink());
- license = profile.getLicense();
- binding.myData.cbLicenseCC0.setChecked(license == License.CC0);
- binding.myData.cbOwnPhoto.setChecked(profile.getPhotoOwner());
- binding.myData.cbAnonymous.setChecked(profile.getAnonymous());
- onAnonymousChecked(null);
-
- if (profile.getEmailVerified()) {
- binding.myData.tvEmailVerification.setText(R.string.emailVerified);
- binding.myData.tvEmailVerification.setTextColor(getResources().getColor(R.color.emailVerified, null));
- } else {
- binding.myData.tvEmailVerification.setText(R.string.emailUnverified);
- binding.myData.tvEmailVerification.setTextColor(getResources().getColor(R.color.emailUnverified, null));
- }
- this.profile = profile;
- }
-
- private void loadRemoteProfile() {
- binding.myData.loginForm.setVisibility(View.VISIBLE);
- binding.myData.profileForm.setVisibility(View.GONE);
- binding.myData.progressBar.setVisibility(View.VISIBLE);
-
- rsapiClient.getProfile().enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- binding.myData.progressBar.setVisibility(View.GONE);
-
- switch (response.code()) {
- case 200:
- Log.i(TAG, "Successfully loaded profile");
- var remoteProfile = response.body();
- if (remoteProfile != null) {
- saveLocalProfile(remoteProfile);
- showProfileView();
- }
- break;
- case 401:
- logout(null);
- SimpleDialogs.confirmOk(MyDataActivity.this, R.string.authorization_failed);
- break;
- default:
- SimpleDialogs.confirmOk(MyDataActivity.this,
- getString(R.string.read_profile_failed, String.valueOf(response.code())));
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- binding.myData.progressBar.setVisibility(View.GONE);
- SimpleDialogs.confirmOk(MyDataActivity.this,
- getString(R.string.read_profile_failed, t.getMessage()));
- }
- });
- }
-
- private void showProfileView() {
- binding.myData.loginForm.setVisibility(View.GONE);
- binding.myData.profileForm.setVisibility(View.VISIBLE);
- Objects.requireNonNull(getSupportActionBar()).setTitle(R.string.tvProfile);
- binding.myData.btProfileSave.setText(R.string.bt_mydata_commit);
- binding.myData.btLogout.setVisibility(View.VISIBLE);
- binding.myData.btChangePassword.setVisibility(View.VISIBLE);
- }
-
- private void oauthAuthorizationCallback(Intent intent) {
- if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) {
- var data = intent.getData();
- if (data != null) {
- if (data.toString().startsWith(baseApplication.getRsapiClient().getRedirectUri())) {
- var code = data.getQueryParameter("code");
- if (code != null) {
- baseApplication.getRsapiClient().requestAccessToken(code).enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- var token = response.body();
- Log.d(TAG, String.valueOf(token));
- if (token != null) {
- baseApplication.setAccessToken(token.getAccessToken());
- baseApplication.getRsapiClient().setToken(token);
- loadRemoteProfile();
- } else {
- Toast.makeText(MyDataActivity.this, getString(R.string.authorization_error, "no token"), Toast.LENGTH_LONG).show();
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- Toast.makeText(MyDataActivity.this, getString(R.string.authorization_error, t.getMessage()), Toast.LENGTH_LONG).show();
- }
- });
- } else {
- String error = data.getQueryParameter("error");
- if (error != null) {
- Toast.makeText(this, getString(R.string.authorization_error, error), Toast.LENGTH_LONG).show();
- }
- }
-
- if (isLoginDataAvailable()) {
- loadRemoteProfile();
- }
- }
- }
- }
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- oauthAuthorizationCallback(intent);
- }
-
- public void selectLicense(View view) {
- license = binding.myData.cbLicenseCC0.isChecked() ? License.CC0 : License.UNKNOWN;
- if (license != License.CC0) {
- SimpleDialogs.confirmOk(this, R.string.cc0_needed);
- }
- }
-
- public void save(View view) {
- profile = createProfileFromUI();
- if (!isValid(profile)) {
- return;
- }
- if (rsapiClient.hasToken()) {
- binding.myData.progressBar.setVisibility(View.VISIBLE);
-
- rsapiClient.saveProfile(profile).enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- binding.myData.progressBar.setVisibility(View.GONE);
-
- switch (response.code()) {
- case 200:
- Log.i(TAG, "Successfully saved profile");
- break;
- case 202:
- SimpleDialogs.confirmOk(MyDataActivity.this, R.string.password_email);
- break;
- case 400:
- SimpleDialogs.confirmOk(MyDataActivity.this, R.string.profile_wrong_data);
- break;
- case 401:
- logout(view);
- SimpleDialogs.confirmOk(MyDataActivity.this, R.string.authorization_failed);
- break;
- case 409:
- SimpleDialogs.confirmOk(MyDataActivity.this, R.string.profile_conflict);
- break;
- default:
- SimpleDialogs.confirmOk(MyDataActivity.this,
- getString(R.string.save_profile_failed, String.valueOf(response.code())));
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- binding.myData.progressBar.setVisibility(View.GONE);
- Log.e(TAG, "Error uploading profile", t);
- SimpleDialogs.confirmOk(MyDataActivity.this,
- getString(R.string.save_profile_failed, t.getMessage()));
- }
- });
- }
-
- saveLocalProfile(profile);
- Toast.makeText(this, R.string.preferences_saved, Toast.LENGTH_LONG).show();
- }
-
- private Profile createProfileFromUI() {
- var newProfile = new Profile(
- binding.myData.etNickname.getText().toString().trim(),
- license,
- binding.myData.cbOwnPhoto.isChecked(),
- binding.myData.cbAnonymous.isChecked(),
- binding.myData.etLinking.getText().toString().trim(),
- binding.myData.etEmail.getText().toString().trim()
- );
-
- if (this.profile != null) {
- newProfile.setEmailVerified(this.profile.getEmailVerified());
- }
-
- return newProfile;
- }
-
- private void saveLocalProfile(Profile profile) {
- baseApplication.setProfile(profile);
- setProfileToUI(profile);
- }
-
- private boolean isLoginDataAvailable() {
- return baseApplication.getAccessToken() != null;
- }
-
- @Override
- public void onBackPressed() {
- startActivity(new Intent(this, MainActivity.class));
- finish();
- }
-
- public boolean isValid(Profile profile) {
- if (StringUtils.isBlank(profile.getNickname())) {
- SimpleDialogs.confirmOk(this, R.string.missing_nickname);
- return false;
- }
- if (!isValidEmail(profile.getEmail())) {
- SimpleDialogs.confirmOk(this, R.string.missing_email_address);
- return false;
- }
- String url = profile.getLink();
- if (StringUtils.isNotBlank(url) && !isValidHTTPURL(url)) {
- SimpleDialogs.confirmOk(this, R.string.missing_link);
- return false;
- }
-
- return true;
- }
-
- private boolean isValidHTTPURL(String urlString) {
- try {
- var url = new URL(urlString);
- if (!"http".equals(url.getProtocol()) && !"https".equals(url.getProtocol())) {
- return false;
- }
- } catch (MalformedURLException e) {
- return false;
- }
- return true;
- }
-
- public boolean isValidEmail(CharSequence target) {
- return target != null && android.util.Patterns.EMAIL_ADDRESS.matcher(target).matches();
-
- }
-
- public void onAnonymousChecked(View view) {
- if (binding.myData.cbAnonymous.isChecked()) {
- binding.myData.etLinking.setVisibility(View.GONE);
- binding.myData.tvLinking.setVisibility(View.GONE);
- } else {
- binding.myData.etLinking.setVisibility(View.VISIBLE);
- binding.myData.tvLinking.setVisibility(View.VISIBLE);
- }
- }
-
- public void login(View view) throws NoSuchAlgorithmException {
- Intent intent = new Intent(
- Intent.ACTION_VIEW,
- rsapiClient.createAuthorizeUri());
- startActivity(intent);
- finish();
- }
-
- public void logout(View view) {
- baseApplication.setAccessToken(null);
- rsapiClient.clearToken();
- profile = new Profile();
- saveLocalProfile(profile);
- binding.myData.profileForm.setVisibility(View.GONE);
- binding.myData.loginForm.setVisibility(View.VISIBLE);
- Objects.requireNonNull(getSupportActionBar()).setTitle(R.string.login);
- }
-
- public void deleteAccount(View view) {
- SimpleDialogs.confirmOkCancel(this, R.string.deleteAccountConfirmation, (d, i) -> rsapiClient.deleteAccount().enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- switch (response.code()) {
- case 204:
- Log.i(TAG, "Successfully deleted account");
- logout(view);
- SimpleDialogs.confirmOk(MyDataActivity.this, R.string.account_deleted);
- break;
- case 401:
- SimpleDialogs.confirmOk(MyDataActivity.this, R.string.authorization_failed);
- logout(view);
- break;
- default:
- SimpleDialogs.confirmOk(MyDataActivity.this,
- getString(R.string.account_deletion_failed, String.valueOf(response.code())));
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- Log.e(TAG, "Error deleting account", t);
- SimpleDialogs.confirmOk(MyDataActivity.this,
- getString(R.string.account_deletion_failed, t.getMessage()));
- }
- }));
- }
-
- public void changePassword(View view) {
- var builder = new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AlertDialogCustom));
- var passwordBinding = ChangePasswordBinding.inflate(getLayoutInflater());
-
- builder.setTitle(R.string.bt_change_password)
- .setView(passwordBinding.getRoot())
- .setIcon(R.mipmap.ic_launcher)
- .setPositiveButton(android.R.string.ok, null)
- .setNegativeButton(android.R.string.cancel, (dialog, id) -> dialog.cancel());
-
- var alertDialog = builder.create();
- alertDialog.show();
-
- alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
- String newPassword = getValidPassword(passwordBinding.password, passwordBinding.passwordRepeat);
- if (newPassword == null) {
- return;
- }
- alertDialog.dismiss();
-
- try {
- newPassword = URLEncoder.encode(newPassword, String.valueOf(StandardCharsets.UTF_8));
- } catch (UnsupportedEncodingException e) {
- Log.e(TAG, "Error encoding new password", e);
- }
-
- rsapiClient.changePassword(newPassword).enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- switch (response.code()) {
- case 200:
- Log.i(TAG, "Successfully changed password");
- logout(view);
- SimpleDialogs.confirmOk(MyDataActivity.this, R.string.password_changed);
- break;
- case 401:
- SimpleDialogs.confirmOk(MyDataActivity.this, R.string.authorization_failed);
- logout(view);
- break;
- default:
- SimpleDialogs.confirmOk(MyDataActivity.this,
- getString(R.string.change_password_failed, String.valueOf(response.code())));
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- Log.e(TAG, "Error changing password", t);
- SimpleDialogs.confirmOk(MyDataActivity.this,
- getString(R.string.change_password_failed, t.getMessage()));
- }
- });
- });
-
- }
-
- private String getValidPassword(EditText etNewPassword, EditText etPasswordRepeat) {
- var newPassword = etNewPassword.getText().toString().trim();
-
- if (newPassword.length() < 8) {
- Toast.makeText(MyDataActivity.this, R.string.password_too_short, Toast.LENGTH_LONG).show();
- return null;
- }
- if (!newPassword.equals(etPasswordRepeat.getText().toString().trim())) {
- Toast.makeText(MyDataActivity.this, R.string.password_repeat_fail, Toast.LENGTH_LONG).show();
- return null;
- }
- return newPassword;
- }
-
- public void requestEmailVerification(View view) {
- SimpleDialogs.confirmOkCancel(this, R.string.requestEmailVerification, (dialogInterface, i) -> rsapiClient.resendEmailVerification().enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- if (response.code() == 200) {
- Log.i(TAG, "Successfully requested email verification");
- Toast.makeText(MyDataActivity.this, R.string.emailVerificationRequested, Toast.LENGTH_LONG).show();
- } else {
- Toast.makeText(MyDataActivity.this, R.string.emailVerificationRequestFailed, Toast.LENGTH_LONG).show();
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- Log.e(TAG, "Error requesting email verification", t);
- Toast.makeText(MyDataActivity.this, R.string.emailVerificationRequestFailed, Toast.LENGTH_LONG).show();
- }
- }));
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/NearbyNotificationService.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/NearbyNotificationService.java
deleted file mode 100644
index f6555c92..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/NearbyNotificationService.java
+++ /dev/null
@@ -1,330 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import static android.app.PendingIntent.FLAG_IMMUTABLE;
-
-import android.Manifest;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.location.Location;
-import android.location.LocationListener;
-import android.location.LocationManager;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.content.ContextCompat;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.db.DbAdapter;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.notification.NearbyBahnhofNotificationManager;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.notification.NearbyBahnhofNotificationManagerFactory;
-
-public class NearbyNotificationService extends Service implements LocationListener {
-
- // The minimum distance to change Updates in meters
- private static final long MIN_DISTANCE_CHANGE_FOR_UPDATES = 1000; // 1km
-
- // The minimum time between updates in milliseconds
- private static final long MIN_TIME_BW_UPDATES = 10000; // 10 seconds
-
- private static final double MIN_NOTIFICATION_DISTANCE = 1.0d; // km
- private static final double EARTH_CIRCUMFERENCE = 40075.017d; // km at equator
- private static final int ONGOING_NOTIFICATION_ID = 0xdeadbeef;
-
- private final String TAG = NearbyNotificationService.class.getSimpleName();
- private List nearStations;
- private Location myPos = new Location((String)null);
-
- private LocationManager locationManager;
-
- private NearbyBahnhofNotificationManager notifiedStationManager;
- private BaseApplication baseApplication = null;
- private DbAdapter dbAdapter = null;
-
- /**
- * The intent action to use to bind to this service's status interface.
- */
- public static final String STATUS_INTERFACE = NearbyNotificationService.class.getPackage().getName() + ".Status";
-
- public NearbyNotificationService() {
- }
-
- @Override
- public void onCreate() {
- Log.i(TAG, "About to create");
- super.onCreate();
- myPos.setLatitude(50d);
- myPos.setLongitude(8d);
- nearStations = new ArrayList<>(0); // no markers until we know where we are
- notifiedStationManager = null;
- baseApplication = (BaseApplication)getApplication();
- dbAdapter = baseApplication.getDbAdapter();
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- cancelNotification();
-
- Log.i(TAG, "Received start command");
-
- var resultIntent = new Intent(this, MainActivity.class);
- var resultPendingIntent =
- PendingIntent.getActivity(
- this,
- 0,
- resultIntent,
- PendingIntent.FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE
- );
-
- NearbyBahnhofNotificationManager.createChannel(this);
-
- // show a permanent notification to indicate that position detection is running
- var ongoingNotification = new NotificationCompat.Builder(this, NearbyBahnhofNotificationManager.CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_launcher)
- .setContentTitle(getString(R.string.nearby_notification_active))
- .setOngoing(true)
- .setLocalOnly(true)
- .setContentIntent(resultPendingIntent)
- .build();
- var notificationManager = NotificationManagerCompat.from(this);
- notificationManager.notify(ONGOING_NOTIFICATION_ID, ongoingNotification);
-
- registerLocationManager();
-
- return START_STICKY;
- }
-
- @Override
- public void onLowMemory() {
- // stop tracking
- super.onLowMemory();
- stopSelf();
- }
-
- @Override
- public void onDestroy() {
- Log.i(TAG, "Service gets destroyed");
- try {
- cancelNotification();
- unregisterLocationManager();
- } catch (Throwable t) {
- Log.wtf(TAG, "Unknown problem when trying to de-register from GPS updates", t);
- }
-
- // Cancel the ongoing notification
- var notificationManager =
- NotificationManagerCompat.from(this);
- notificationManager.cancel(ONGOING_NOTIFICATION_ID);
-
- super.onDestroy();
- }
-
- private void cancelNotification() {
- if (notifiedStationManager != null) {
- notifiedStationManager.destroy();
- notifiedStationManager = null;
- }
- }
-
- @Override
- public void onLocationChanged(@NonNull Location location) {
- Log.i(TAG, "Received new location: " + location);
- try {
- myPos = location;
-
- // check if currently advertised station is still in range
- if (notifiedStationManager != null) {
- if (calcDistance(notifiedStationManager.getStation()) > MIN_NOTIFICATION_DISTANCE) {
- cancelNotification();
- }
- }
- Log.d(TAG, "Reading matching stations from local database");
- readStations();
-
- // check if a user notification is appropriate
- checkNearestStation();
- } catch (Throwable t) {
- Log.e(TAG, "Unknown Problem arised during location change handling", t);
- }
- }
-
- @Override
- public void onStatusChanged(String provider, int status, Bundle extras) {
-
- }
-
- @Override
- public void onProviderEnabled(@NonNull String provider) {
-
- }
-
- @Override
- public void onProviderDisabled(@NonNull String provider) {
-
- }
-
- private void checkNearestStation() {
- double minDist = 3e3;
- Station nearest = null;
- for (var station : nearStations) {
- var dist = calcDistance(station);
- if (dist < minDist) {
- nearest = station;
- minDist = dist;
- }
- }
- if (nearest != null && minDist < MIN_NOTIFICATION_DISTANCE) {
- notifyNearest(nearest, minDist);
- Log.i(TAG, "Issued notification to user");
- } else {
- Log.d(TAG, "No notification - nearest station was " + minDist + " km away: " + nearest);
- }
- }
-
- /**
- * Start notification build process. This might run asynchronous in case that required
- * photos need to be fetched fist. If the notification is built up, #onNotificationReady(Notification)
- * will be called.
- *
- * @param nearest the station nearest to the current position
- * @param distance the distance of the station to the current position
- */
- private void notifyNearest(Station nearest, double distance) {
- if (notifiedStationManager != null) {
- if (notifiedStationManager.getStation().equals(nearest)) {
- return; // Notification für diesen Bahnhof schon angezeigt
- } else {
- notifiedStationManager.destroy();
- notifiedStationManager = null;
- }
- }
- notifiedStationManager = NearbyBahnhofNotificationManagerFactory.create(this, nearest, distance, dbAdapter.fetchCountriesWithProviderApps(baseApplication.getCountryCodes()));
- notifiedStationManager.notifyUser();
- }
-
- /**
- * Calculate the distance between the given station and our current position (myLatitude, myLongitude)
- *
- * @param station the station to calculate the distance to
- * @return the distance
- */
- private double calcDistance(Station station) {
- // Wir nähern für glatte Oberflächen, denn wir sind an Abständen kleiner 1km interessiert
- var lateralDiff = myPos.getLatitude() - station.getLat();
- var longDiff = (Math.abs(myPos.getLatitude()) < 89.99d) ?
- (myPos.getLongitude() - station.getLon()) * Math.cos(myPos.getLatitude() / 180 * Math.PI) :
- 0.0d; // at the poles, longitude doesn't matter
- // simple Pythagoras now.
- return Math.sqrt(Math.pow(lateralDiff, 2.0d) + Math.pow(longDiff, 2.0d)) * EARTH_CIRCUMFERENCE / 360.0d;
- }
-
- public void registerLocationManager() {
-
- try {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
- Log.w(TAG, "No Location Permission");
- return;
- }
-
- locationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
-
- // getting GPS status
- var isGPSEnabled = locationManager
- .isProviderEnabled(LocationManager.GPS_PROVIDER);
-
- // if GPS Enabled get lat/long using GPS Services
- if (isGPSEnabled) {
- locationManager.requestLocationUpdates(
- LocationManager.GPS_PROVIDER,
- MIN_TIME_BW_UPDATES,
- MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
- Log.d(TAG, "GPS Enabled");
- if (locationManager != null) {
- myPos = locationManager
- .getLastKnownLocation(LocationManager.GPS_PROVIDER);
- }
- } else {
- // getting network status
- var isNetworkEnabled = locationManager
- .isProviderEnabled(LocationManager.NETWORK_PROVIDER);
-
- // First get location from Network Provider
- if (isNetworkEnabled) {
- locationManager.requestLocationUpdates(
- LocationManager.NETWORK_PROVIDER,
- MIN_TIME_BW_UPDATES,
- MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
- Log.d(TAG, "Network Location enabled");
- if (locationManager != null) {
- myPos = locationManager
- .getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
- }
- }
- }
- } catch (Exception e) {
- Log.e(TAG, "Error registering LocationManager", e);
- var b = new Bundle();
- b.putString("error", "Error registering LocationManager: " + e);
- locationManager = null;
- myPos = null;
- return;
- }
- Log.i(TAG, "LocationManager registered");
- }
-
- private void unregisterLocationManager() {
- if (locationManager != null) {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
- locationManager.removeUpdates(this);
- }
- locationManager = null;
- }
- Log.i(TAG, "LocationManager unregistered");
- }
-
- /**
- * Class returned when an activity binds to this service.
- * Currently, can only be used to query the service state, i.e. if the location tracking
- * is switched off or on with photo or on without photo.
- */
- public static class StatusBinder extends Binder {
- }
-
- /**
- * Bind to interfaces provided by this service. Currently implemented:
- *
- * - STATUS_INTERFACE: Returns a StatusBinder that can be used to query the tracking status
- *
- *
- * @param intent an Intent giving the intended action
- * @return a Binder instance suitable for the intent supplied, or null if none matches.
- */
- @Override
- public IBinder onBind(Intent intent) {
- if (STATUS_INTERFACE.equals(intent.getAction())) {
- return new StatusBinder();
- } else {
- return null;
- }
- }
-
- private void readStations() {
- try {
- Log.i(TAG, "Lade nahegelegene Bahnhoefe");
- nearStations = dbAdapter.getStationByLatLngRectangle(myPos.getLatitude(), myPos.getLongitude(), baseApplication.getStationFilter());
- } catch (Exception e) {
- Log.e(TAG, "Datenbank konnte nicht geöffnet werden", e);
- }
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/OutboxActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/OutboxActivity.java
deleted file mode 100644
index f68efc0b..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/OutboxActivity.java
+++ /dev/null
@@ -1,151 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import static java.util.stream.Collectors.toList;
-
-import android.app.AlertDialog;
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.ContextThemeWrapper;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-
-import java.util.List;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityOutboxBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.db.DbAdapter;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.db.OutboxAdapter;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.InboxStateQuery;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Upload;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.FileUtils;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-public class OutboxActivity extends AppCompatActivity {
-
- private static final String TAG = OutboxActivity.class.getSimpleName();
-
- private OutboxAdapter adapter;
- private DbAdapter dbAdapter;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- var baseApplication = (BaseApplication) getApplication();
- dbAdapter = baseApplication.getDbAdapter();
-
- var binding = ActivityOutboxBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- adapter = new OutboxAdapter(OutboxActivity.this, dbAdapter.getOutbox());
- binding.lstUploads.setAdapter(adapter);
-
- // item click
- binding.lstUploads.setOnItemClickListener((parent, view, position, id) -> {
- var upload = dbAdapter.getUploadById(id);
- Intent intent;
- if (upload.isProblemReport()) {
- intent = new Intent(OutboxActivity.this, ProblemReportActivity.class);
- intent.putExtra(ProblemReportActivity.EXTRA_UPLOAD, upload);
- } else {
- intent = new Intent(OutboxActivity.this, UploadActivity.class);
- intent.putExtra(UploadActivity.EXTRA_UPLOAD, upload);
- }
- startActivity(intent);
- });
-
- binding.lstUploads.setOnItemLongClickListener((parent, view, position, id) -> {
- var uploadId = String.valueOf(id);
- SimpleDialogs.confirmOkCancel(OutboxActivity.this, getResources().getString(R.string.delete_upload, uploadId), (dialog, which) -> {
- dbAdapter.deleteUpload(id);
- FileUtils.deleteQuietly(FileUtils.getStoredMediaFile(this, id));
- adapter.changeCursor(dbAdapter.getOutbox());
- });
- return true;
- });
-
- var query = dbAdapter.getPendingUploads(true).stream()
- .map(upload -> new InboxStateQuery(upload.getRemoteId()))
- .collect(toList());
-
- baseApplication.getRsapiClient().queryUploadState(query).enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call> call, @NonNull Response> response) {
- if (response.isSuccessful()) {
- var stateQueries = response.body();
- if (stateQueries != null) {
- dbAdapter.updateUploadStates(stateQueries);
- adapter.changeCursor(dbAdapter.getOutbox());
- }
- } else if (response.code() == 401) {
- baseApplication.setAccessToken(null);
- baseApplication.getRsapiClient().clearToken();
- Toast.makeText(OutboxActivity.this, R.string.authorization_failed, Toast.LENGTH_LONG).show();
- startActivity(new Intent(OutboxActivity.this, MyDataActivity.class));
- finish();
- } else {
- Log.w(TAG, "Upload states not processable");
- }
- }
-
- @Override
- public void onFailure(@NonNull Call> call, @NonNull Throwable t) {
- Log.e(TAG, "Error retrieving upload state", t);
- Toast.makeText(OutboxActivity.this,
- R.string.error_retrieving_upload_state,
- Toast.LENGTH_LONG).show();
- }
- });
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- menu.clear();
- getMenuInflater().inflate(R.menu.outbox, menu);
- return super.onPrepareOptionsMenu(menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == R.id.nav_delete_processed_uploads) {
- deleteCompletedUploads();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- private void deleteCompletedUploads() {
- var uploads = dbAdapter.getCompletedUploads();
- if (uploads.isEmpty()) {
- return;
- }
-
- new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AlertDialogCustom))
- .setIcon(R.mipmap.ic_launcher)
- .setTitle(R.string.confirm_delete_processed_uploads)
- .setPositiveButton(R.string.button_ok_text, (dialog, which) -> {
- for (Upload upload : uploads) {
- dbAdapter.deleteUpload(upload.getId());
- FileUtils.deleteQuietly(FileUtils.getStoredMediaFile(this, upload.getId()));
- }
- adapter.changeCursor(dbAdapter.getOutbox());
- })
- .setNegativeButton(R.string.button_cancel_text, null)
- .create().show();
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- adapter.changeCursor(dbAdapter.getOutbox());
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/PhotoPagerAdapter.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/PhotoPagerAdapter.java
deleted file mode 100644
index 2e3cb7fa..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/PhotoPagerAdapter.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.github.chrisbanes.photoview.PhotoView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PageablePhoto;
-
-public class PhotoPagerAdapter extends RecyclerView.Adapter {
-
- private final List pageablePhotos = new ArrayList<>();
-
- private final Context context;
-
- public PhotoPagerAdapter(Context context) {
- this.context = context;
- }
-
- public int addPageablePhoto(PageablePhoto pageablePhoto) {
- pageablePhotos.add(pageablePhoto);
- notifyDataSetChanged();
- return pageablePhotos.size() - 1;
- }
-
- @NonNull
- @Override
- public PhotoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- var view = LayoutInflater.from(context).inflate(R.layout.photo_view_item, parent, false);
- return new PhotoViewHolder(view);
- }
-
- @Override
- public void onBindViewHolder(@NonNull PhotoViewHolder holder, int position) {
- var pageablePhoto = getPageablePhotoAtPosition(position);
- if (pageablePhoto == null) {
- holder.photoView.setImageResource(R.drawable.photo_missing);
- } else {
- holder.photoView.setImageBitmap(pageablePhoto.getBitmap());
- }
- }
-
- public PageablePhoto getPageablePhotoAtPosition(int position) {
- return pageablePhotos.isEmpty() ? null : pageablePhotos.get(position);
- }
-
- @Override
- public int getItemCount() {
- return pageablePhotos.isEmpty() ? 1 : pageablePhotos.size();
- }
-
- public static class PhotoViewHolder extends RecyclerView.ViewHolder {
-
- PhotoView photoView;
-
- public PhotoViewHolder(@NonNull View itemView) {
- super(itemView);
- photoView = itemView.findViewById(R.id.photoView);
- }
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/ProblemReportActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/ProblemReportActivity.java
deleted file mode 100644
index 369193f3..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/ProblemReportActivity.java
+++ /dev/null
@@ -1,321 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import android.app.TaskStackBuilder;
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.EditText;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.NavUtils;
-
-import com.google.gson.Gson;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ReportProblemBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.InboxResponse;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.InboxStateQuery;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.ProblemReport;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.ProblemType;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Upload;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi.RSAPIClient;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-public class ProblemReportActivity extends AppCompatActivity {
-
- private static final String TAG = ProblemReportActivity.class.getSimpleName();
-
- public static final String EXTRA_UPLOAD = "EXTRA_UPLOAD";
- public static final String EXTRA_STATION = "EXTRA_STATION";
- public static final String EXTRA_PHOTO_ID = "EXTRA_PHOTO_ID";
-
- private BaseApplication baseApplication;
- private RSAPIClient rsapiClient;
- private ReportProblemBinding binding;
-
- private Upload upload;
- private Station station;
- private Long photoId;
- private final ArrayList problemTypes = new ArrayList<>();
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- binding = ReportProblemBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- baseApplication = (BaseApplication) getApplication();
- rsapiClient = baseApplication.getRsapiClient();
-
- Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
-
- problemTypes.add(getString(R.string.problem_please_specify));
- for (var type : ProblemType.values()) {
- problemTypes.add(getString(type.getMessageId()));
- }
- var adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, problemTypes);
- adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- binding.problemType.setAdapter(adapter);
-
- binding.problemType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(final AdapterView> parent, final View view, final int position, final long id) {
- if (position > 0) {
- var type = ProblemType.values()[position - 1];
- setCoordsVisible(type == ProblemType.WRONG_LOCATION);
- setTitleVisible(type == ProblemType.WRONG_NAME);
- } else {
- setCoordsVisible(false);
- setTitleVisible(false);
- }
- }
-
- @Override
- public void onNothingSelected(final AdapterView> parent) {
- setCoordsVisible(false);
- setTitleVisible(false);
- }
- });
-
- if (!baseApplication.isLoggedIn()) {
- Toast.makeText(this, R.string.please_login, Toast.LENGTH_LONG).show();
- startActivity(new Intent(ProblemReportActivity.this, MyDataActivity.class));
- finish();
- return;
- }
-
- if (!baseApplication.getProfile().getEmailVerified()) {
- SimpleDialogs.confirmOk(this, R.string.email_unverified_for_problem_report, (dialog, view) -> {
- startActivity(new Intent(ProblemReportActivity.this, MyDataActivity.class));
- finish();
- });
- return;
- }
-
- onNewIntent(getIntent());
- }
-
- private void setCoordsVisible(boolean visible) {
- binding.tvNewCoords.setVisibility(visible ? View.VISIBLE : View.GONE);
- binding.etNewLatitude.setVisibility(visible ? View.VISIBLE : View.GONE);
- binding.etNewLongitude.setVisibility(visible ? View.VISIBLE : View.GONE);
- }
-
- private void setTitleVisible(boolean visible) {
- binding.tvNewTitle.setVisibility(visible ? View.VISIBLE : View.GONE);
- binding.etNewTitle.setVisibility(visible ? View.VISIBLE : View.GONE);
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
-
- if (intent != null) {
- upload = (Upload) intent.getSerializableExtra(EXTRA_UPLOAD);
- station = (Station) intent.getSerializableExtra(EXTRA_STATION);
- photoId = (Long) intent.getSerializableExtra(EXTRA_PHOTO_ID);
-
- if (upload != null && upload.isProblemReport()) {
- binding.etProblemComment.setText(upload.getComment());
- binding.etNewLatitude.setText(upload.getLat() != null ? upload.getLat().toString() : "");
- binding.etNewLongitude.setText(upload.getLon() != null ? upload.getLon().toString() : "");
-
- int selected = upload.getProblemType().ordinal() + 1;
- binding.problemType.setSelection(selected);
-
- if (station == null) {
- station = baseApplication.getDbAdapter().getStationForUpload(upload);
- }
-
- fetchUploadStatus(upload);
- }
-
- if (station != null) {
- binding.tvStationTitle.setText(station.getTitle());
- binding.etNewTitle.setText(station.getTitle());
- binding.etNewLatitude.setText(String.valueOf(station.getLat()));
- binding.etNewLongitude.setText(String.valueOf(station.getLon()));
- }
- }
- }
-
- public void reportProblem(View view) {
- int selectedType = binding.problemType.getSelectedItemPosition();
- if (selectedType == 0) {
- Toast.makeText(getApplicationContext(), getString(R.string.problem_please_specify), Toast.LENGTH_LONG).show();
- return;
- }
- var type = ProblemType.values()[selectedType - 1];
- var comment = binding.etProblemComment.getText().toString();
- if (StringUtils.isBlank(comment)) {
- Toast.makeText(getApplicationContext(), getString(R.string.problem_please_comment), Toast.LENGTH_LONG).show();
- return;
- }
-
- Double lat = null;
- Double lon = null;
- if (binding.etNewLatitude.getVisibility() == View.VISIBLE) {
- lat = parseDouble(binding.etNewLatitude);
- }
- if (binding.etNewLongitude.getVisibility() == View.VISIBLE) {
- lon = parseDouble(binding.etNewLongitude);
- }
- if (type == ProblemType.WRONG_LOCATION && (lat == null || lon == null)) {
- Toast.makeText(getApplicationContext(), getString(R.string.problem_wrong_lat_lon), Toast.LENGTH_LONG).show();
- return;
- }
- var title = binding.etNewTitle.getText().toString();
- if (type == ProblemType.WRONG_NAME && (StringUtils.isBlank(title) || Objects.equals(station.getTitle(), title))) {
- Toast.makeText(getApplicationContext(), getString(R.string.problem_please_provide_corrected_title), Toast.LENGTH_LONG).show();
- return;
- }
-
- upload = new Upload(
- null,
- station.getCountry(),
- station.getId(),
- null,
- title,
- lat,
- lon,
- comment,
- null,
- type
- );
-
- upload = baseApplication.getDbAdapter().insertUpload(upload);
-
- var problemReport = new ProblemReport(
- station.getCountry(),
- station.getId(),
- comment,
- type,
- photoId,
- lat,
- lon,
- title);
-
- SimpleDialogs.confirmOkCancel(ProblemReportActivity.this, R.string.send_problem_report,
- (dialog, which) -> rsapiClient.reportProblem(problemReport).enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- InboxResponse inboxResponse;
- if (response.isSuccessful()) {
- inboxResponse = response.body();
- } else if (response.code() == 401) {
- onUnauthorized();
- return;
- } else {
- inboxResponse = new Gson().fromJson(response.errorBody().charStream(), InboxResponse.class);
- }
-
- upload.setRemoteId(inboxResponse.getId());
- upload.setUploadState(inboxResponse.getState().getUploadState());
- baseApplication.getDbAdapter().updateUpload(upload);
- if (inboxResponse.getState() == InboxResponse.InboxResponseState.ERROR) {
- SimpleDialogs.confirmOk(ProblemReportActivity.this,
- getString(InboxResponse.InboxResponseState.ERROR.getMessageId(), inboxResponse.getMessage()));
- } else {
- SimpleDialogs.confirmOk(ProblemReportActivity.this, inboxResponse.getState().getMessageId());
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- Log.e(TAG, "Error reporting problem", t);
- }
- }));
- }
-
- private void onUnauthorized() {
- baseApplication.setAccessToken(null);
- rsapiClient.clearToken();
- Toast.makeText(ProblemReportActivity.this, R.string.authorization_failed, Toast.LENGTH_LONG).show();
- startActivity(new Intent(ProblemReportActivity.this, MyDataActivity.class));
- finish();
- }
-
- private Double parseDouble(final EditText editText) {
- try {
- return Double.parseDouble(String.valueOf(editText.getText()));
- } catch (Exception e) {
- Log.e(TAG, "error parsing double " + editText.getText(), e);
- }
- return null;
- }
-
- private void fetchUploadStatus(Upload upload) {
- if (upload == null || upload.getRemoteId() == null) {
- return;
- }
-
- var stateQuery = new InboxStateQuery(
- upload.getRemoteId(),
- upload.getCountry(),
- upload.getStationId());
-
- rsapiClient.queryUploadState(List.of(stateQuery)).enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call> call, @NonNull Response> response) {
- if (response.isSuccessful()) {
- var stateQueries = response.body();
- if (stateQueries != null && !stateQueries.isEmpty()) {
- var stateQuery = stateQueries.get(0);
- binding.uploadStatus.setText(getString(R.string.upload_state, getString(stateQuery.getState().getTextId())));
- binding.uploadStatus.setTextColor(getResources().getColor(stateQuery.getState().getColorId(), null));
- upload.setUploadState(stateQuery.getState());
- upload.setRejectReason(stateQuery.getRejectedReason());
- upload.setCrc32(stateQuery.getCrc32());
- upload.setRemoteId(stateQuery.getId());
- baseApplication.getDbAdapter().updateUpload(upload);
- }
- } else if (response.code() == 401) {
- onUnauthorized();
- } else {
- Log.w(TAG, "Upload states not processable");
- }
- }
-
- @Override
- public void onFailure(@NonNull Call> call, @NonNull Throwable t) {
- Log.e(TAG, "Error retrieving upload state", t);
- }
- });
-
- }
-
- @Override
- public void onBackPressed() {
- navigateUp();
- }
-
- public void navigateUp() {
- var callingActivity = getCallingActivity(); // if MapsActivity was calling, then we don't want to rebuild the Backstack
- if (callingActivity == null) {
- var upIntent = NavUtils.getParentActivityIntent(this);
- assert upIntent != null;
- upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
- if (NavUtils.shouldUpRecreateTask(this, upIntent) || isTaskRoot()) {
- Log.v(TAG, "Recreate back stack");
- TaskStackBuilder.create(this).addNextIntentWithParentStack(upIntent).startActivities();
- }
- }
-
- finish();
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/ShowErrorActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/ShowErrorActivity.java
deleted file mode 100644
index 9f688109..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/ShowErrorActivity.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import androidx.appcompat.app.AppCompatActivity;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityShowErrorBinding;
-
-public class ShowErrorActivity extends AppCompatActivity {
-
- public static final String EXTRA_ERROR_TEXT = "error";
-
- private ActivityShowErrorBinding binding;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- binding = ActivityShowErrorBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- binding.textViewError.setText(getIntent().getStringExtra(EXTRA_ERROR_TEXT));
-
- setSupportActionBar(binding.mapsToolbar);
- if (getSupportActionBar() != null) {
- getSupportActionBar().setTitle(createErrorTitle());
- }
- }
-
- private String createErrorTitle() {
- return String.format(getString(R.string.error_crash_title), getString(R.string.app_name));
- }
-
- private void reportBug() {
- Uri uriUrl;
- try {
- uriUrl = Uri.parse(
- String.format(
- getString(R.string.report_issue_link),
- URLEncoder.encode(binding.textViewError.getText().toString(), StandardCharsets.UTF_8.toString())
- )
- );
- } catch (UnsupportedEncodingException ignored) {
- // can't happen as UTF-8 is always available
- return;
- }
- var intent = new Intent(Intent.ACTION_VIEW, uriUrl);
- startActivity(intent);
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.show_error, menu);
- return super.onCreateOptionsMenu(menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == R.id.error_share) {
- onClickedShare();
- return true;
- } else if (item.getItemId() == R.id.error_report) {
- reportBug();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- private void onClickedShare() {
- var intent = new Intent(Intent.ACTION_SEND);
- intent.putExtra(Intent.EXTRA_SUBJECT, createErrorTitle());
- intent.putExtra(Intent.EXTRA_TEXT, binding.textViewError.getText());
- intent.setType("text/plain");
- startActivity(intent);
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/UploadActivity.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/UploadActivity.java
deleted file mode 100644
index 442f30db..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/UploadActivity.java
+++ /dev/null
@@ -1,602 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos;
-
-import static android.content.Intent.createChooser;
-
-import android.app.Activity;
-import android.app.TaskStackBuilder;
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.provider.MediaStore;
-import android.text.Html;
-import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.Toast;
-
-import androidx.activity.OnBackPressedCallback;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.ActivityCompat;
-import androidx.core.app.NavUtils;
-import androidx.core.content.FileProvider;
-
-import com.google.gson.Gson;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URLConnection;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.zip.CRC32;
-import java.util.zip.CheckedInputStream;
-import java.util.zip.CheckedOutputStream;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityUploadBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.InboxResponse;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.InboxStateQuery;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Upload;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi.RSAPIClient;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.Constants;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.FileUtils;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.KeyValueSpinnerItem;
-import okhttp3.MediaType;
-import okhttp3.RequestBody;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-public class UploadActivity extends AppCompatActivity implements ActivityCompat.OnRequestPermissionsResultCallback {
-
- private static final String TAG = UploadActivity.class.getSimpleName();
-
- // Names of Extras that this class reacts to
- public static final String EXTRA_UPLOAD = "EXTRA_UPLOAD";
- public static final String EXTRA_STATION = "EXTRA_STATION";
- public static final String EXTRA_LATITUDE = "EXTRA_LATITUDE";
- public static final String EXTRA_LONGITUDE = "EXTRA_LONGITUDE";
-
- private BaseApplication baseApplication;
- private RSAPIClient rsapiClient;
-
- private ActivityUploadBinding binding;
-
- private Upload upload;
- private Station station;
- private List countries;
- private Double latitude;
- private Double longitude;
- private String bahnhofId;
- private Long crc32 = null;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- binding = ActivityUploadBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- baseApplication = (BaseApplication) getApplication();
- rsapiClient = baseApplication.getRsapiClient();
- countries = new ArrayList<>(baseApplication.getDbAdapter().getAllCountries());
-
- Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
-
- if (!baseApplication.isLoggedIn()) {
- Toast.makeText(this, R.string.please_login, Toast.LENGTH_LONG).show();
- startActivity(new Intent(this, MyDataActivity.class));
- finish();
- return;
- }
-
- if (!baseApplication.getProfile().isAllowedToUploadPhoto()) {
- SimpleDialogs.confirmOk(this, R.string.no_photo_upload_allowed, (dialog, which) -> {
- startActivity(new Intent(this, MyDataActivity.class));
- finish();
- });
- return;
- }
-
- getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
- @Override
- public void handleOnBackPressed() {
- navigateUp();
- }
- });
-
- onNewIntent(getIntent());
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
-
- if (intent != null) {
- upload = (Upload) intent.getSerializableExtra(EXTRA_UPLOAD);
- station = (Station) intent.getSerializableExtra(EXTRA_STATION);
- latitude = (Double) intent.getSerializableExtra(EXTRA_LATITUDE);
- longitude = (Double) intent.getSerializableExtra(EXTRA_LONGITUDE);
-
- if (station == null && upload != null && upload.isUploadForExistingStation()) {
- station = baseApplication.getDbAdapter().getStationForUpload(upload);
- }
-
- if (latitude == null && longitude == null && upload != null && upload.isUploadForMissingStation()) {
- latitude = upload.getLat();
- longitude = upload.getLon();
- }
-
- if (station == null && (latitude == null || longitude == null)) {
- Log.w(TAG, "EXTRA_STATION and EXTRA_LATITUDE or EXTRA_LONGITUDE in intent data missing");
- Toast.makeText(this, R.string.station_or_coords_not_found, Toast.LENGTH_LONG).show();
- finish();
- return;
- }
-
- if (station != null) {
- bahnhofId = station.getId();
- binding.upload.etStationTitle.setText(station.getTitle());
- binding.upload.etStationTitle.setInputType(EditorInfo.TYPE_NULL);
- binding.upload.etStationTitle.setSingleLine(false);
-
- if (upload == null) {
- upload = baseApplication.getDbAdapter().getPendingUploadsForStation(station).stream()
- .filter(Upload::isPendingPhotoUpload)
- .findFirst()
- .orElse(null);
- }
-
- setLocalBitmap(upload);
-
- binding.upload.spActive.setVisibility(View.GONE);
- binding.upload.spCountries.setVisibility(View.GONE);
-
- String country = station.getCountry();
- updateOverrideLicense(country);
- } else {
- if (upload == null) {
- upload = baseApplication.getDbAdapter().getPendingUploadForCoordinates(latitude, longitude);
- }
-
- binding.upload.etStationTitle.setInputType(EditorInfo.TYPE_CLASS_TEXT);
- binding.upload.spActive.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, getResources().getStringArray(R.array.active_flag_options)));
- if (upload != null) {
- binding.upload.etStationTitle.setText(upload.getTitle());
- setLocalBitmap(upload);
-
- if (upload.getActive() == null) {
- binding.upload.spActive.setSelection(0);
- } else if (upload.getActive()) {
- binding.upload.spActive.setSelection(1);
- } else {
- binding.upload.spActive.setSelection(2);
- }
- } else {
- binding.upload.spActive.setSelection(0);
- }
-
- var items = new KeyValueSpinnerItem[countries.size() + 1];
- items[0] = new KeyValueSpinnerItem(getString(R.string.chooseCountry), "");
- int selected = 0;
-
- for (int i = 0; i < countries.size(); i++) {
- var country = countries.get(i);
- items[i + 1] = new KeyValueSpinnerItem(country.getName(), country.getCode());
- if (upload != null && country.getCode().equals(upload.getCountry())) {
- selected = i + 1;
- }
- }
- var countryAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, items);
- countryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- binding.upload.spCountries.setAdapter(countryAdapter);
- binding.upload.spCountries.setSelection(selected);
- binding.upload.spCountries.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(final AdapterView> parent, final View view, final int position, final long id) {
- var selectedCountry = (KeyValueSpinnerItem) parent.getItemAtPosition(position);
- updateOverrideLicense(selectedCountry.getValue());
- }
-
- @Override
- public void onNothingSelected(final AdapterView> parent) {
- updateOverrideLicense(null);
- }
- });
- }
-
- if (upload != null) {
- binding.upload.etComment.setText(upload.getComment());
- }
-
- binding.upload.txtPanorama.setText(Html.fromHtml(getString(R.string.panorama_info), Html.FROM_HTML_MODE_COMPACT));
- binding.upload.txtPanorama.setMovementMethod(LinkMovementMethod.getInstance());
- binding.upload.txtPanorama.setLinkTextColor(Color.parseColor("#c71c4d"));
-
- }
- }
-
- private void updateOverrideLicense(final String country) {
- var overrideLicense = Country.getCountryByCode(countries, country).map(Country::getOverrideLicense).orElse(null);
- if (overrideLicense != null) {
- binding.upload.cbSpecialLicense.setText(getString(R.string.special_license, overrideLicense));
- }
- binding.upload.cbSpecialLicense.setVisibility(overrideLicense == null ? View.GONE : View.VISIBLE);
- }
-
- private final ActivityResultLauncher imageCaptureResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
- result -> {
- if (result.getResultCode() == Activity.RESULT_OK) {
- try {
- assertCurrentPhotoUploadExists();
- var cameraTempFile = getCameraTempFile();
- var options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true; // just query the image size in the first step
- BitmapFactory.decodeFile(cameraTempFile.getPath(), options);
-
- int sampling = options.outWidth / Constants.STORED_PHOTO_WIDTH;
- if (sampling > 1) {
- options.inSampleSize = sampling;
- }
- options.inJustDecodeBounds = false;
-
- storeBitmapToLocalFile(getStoredMediaFile(upload), BitmapFactory.decodeFile(cameraTempFile.getPath(), options));
- FileUtils.deleteQuietly(cameraTempFile);
- } catch (Exception e) {
- Log.e(TAG, "Error processing photo", e);
- Toast.makeText(getApplicationContext(), getString(R.string.error_processing_photo) + e.getMessage(), Toast.LENGTH_LONG).show();
- }
- }
- });
-
- public void takePicture(View view) {
- if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
- return;
- }
-
- assertCurrentPhotoUploadExists();
- var photoURI = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", getCameraTempFile());
- var intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
- intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
- intent.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, getResources().getString(R.string.app_name));
- intent.putExtra(MediaStore.EXTRA_MEDIA_TITLE, binding.upload.etStationTitle.getText());
- intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- try {
- imageCaptureResultLauncher.launch(intent);
- } catch (ActivityNotFoundException exception) {
- Toast.makeText(this, R.string.no_image_capture_app_found, Toast.LENGTH_LONG).show();
- }
- }
-
- private final ActivityResultLauncher selectPictureResultLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(),
- uri -> {
- try (var parcelFileDescriptor = getContentResolver().openFileDescriptor(uri, "r")) {
- if (parcelFileDescriptor == null) {
- return;
- }
- var fileDescriptor = parcelFileDescriptor.getFileDescriptor();
- assertCurrentPhotoUploadExists();
-
- var bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
- int sampling = bitmap.getWidth() / Constants.STORED_PHOTO_WIDTH;
- var scaledScreen = bitmap;
- if (sampling > 1) {
- scaledScreen = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth() / sampling, bitmap.getHeight() / sampling, false);
- }
-
- storeBitmapToLocalFile(getStoredMediaFile(upload), scaledScreen);
- } catch (Exception e) {
- Log.e(TAG, "Error processing photo", e);
- Toast.makeText(getApplicationContext(), getString(R.string.error_processing_photo) + e.getMessage(), Toast.LENGTH_LONG).show();
- }
- });
-
- public void selectPicture(View view) {
- selectPictureResultLauncher.launch("image/*");
- }
-
- private void assertCurrentPhotoUploadExists() {
- if (upload == null || upload.isProblemReport() || upload.isUploaded()) {
- upload = new Upload(
- null,
- station != null ? station.getCountry() : null,
- station != null ? station.getId() : null,
- null,
- null,
- latitude,
- longitude);
- upload = baseApplication.getDbAdapter().insertUpload(upload);
- }
- }
-
- private void storeBitmapToLocalFile(File file, Bitmap bitmap) throws IOException {
- if (bitmap == null) {
- throw new RuntimeException(getString(R.string.error_scaling_photo));
- }
- Log.i(TAG, "Save photo with width=" + bitmap.getWidth() + " and height=" + bitmap.getHeight() + " to: " + file);
- try (var cos = new CheckedOutputStream(new FileOutputStream(file), new CRC32())) {
- bitmap.compress(Bitmap.CompressFormat.JPEG, Constants.STORED_PHOTO_QUALITY, cos);
- crc32 = cos.getChecksum().getValue();
- setLocalBitmap(upload);
- }
- }
-
- /**
- * Get the file path for storing this stations foto
- *
- * @return the File
- */
- @Nullable
- public File getStoredMediaFile(Upload upload) {
- if (upload == null) {
- return null;
- }
- return FileUtils.getStoredMediaFile(this, upload.getId());
- }
-
- /**
- * Get the file path for the Camera app to store the unprocessed photo to.
- */
- private File getCameraTempFile() {
- return FileUtils.getImageCacheFile(this, String.valueOf(upload.getId()));
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.upload, menu);
- return super.onCreateOptionsMenu(menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- int itemId = item.getItemId();
- if (itemId == R.id.share_photo) {
- var shareIntent = createPhotoSendIntent();
- if (shareIntent != null) {
- shareIntent.putExtra(Intent.EXTRA_TEXT, binding.upload.etStationTitle.getText());
- shareIntent.setType("image/jpeg");
- startActivity(createChooser(shareIntent, getString(R.string.share_photo)));
- }
- } else if (itemId == android.R.id.home) {
- navigateUp();
- } else {
- return super.onOptionsItemSelected(item);
- }
-
- return true;
- }
-
- public void navigateUp() {
- var callingActivity = getCallingActivity(); // if MapsActivity was calling, then we don't want to rebuild the Backstack
- var upIntent = NavUtils.getParentActivityIntent(this);
- if (callingActivity == null && upIntent != null) {
- upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
- if (NavUtils.shouldUpRecreateTask(this, upIntent) || isTaskRoot()) {
- Log.v(TAG, "Recreate back stack");
- TaskStackBuilder.create(this).addNextIntentWithParentStack(upIntent).startActivities();
- }
- }
-
- finish();
- }
-
- public void upload(View view) {
- if (TextUtils.isEmpty(binding.upload.etStationTitle.getText())) {
- Toast.makeText(this, R.string.station_title_needed, Toast.LENGTH_LONG).show();
- return;
- }
-
- assertCurrentPhotoUploadExists();
-
- var mediaFile = getStoredMediaFile(upload);
- assert mediaFile != null;
-
- if (!mediaFile.exists()) {
- if (station != null) {
- Toast.makeText(this, R.string.please_take_photo, Toast.LENGTH_LONG).show();
- return;
- }
- }
-
- if (binding.upload.cbSpecialLicense.getText().length() > 0 && !binding.upload.cbSpecialLicense.isChecked()) {
- Toast.makeText(this, R.string.special_license_confirm, Toast.LENGTH_LONG).show();
- return;
- }
- if (crc32 != null && upload != null && crc32.equals(upload.getCrc32()) && !binding.upload.cbChecksum.isChecked()) {
- Toast.makeText(this, R.string.photo_checksum, Toast.LENGTH_LONG).show();
- return;
- }
- if (station == null) {
- if (binding.upload.spActive.getSelectedItemPosition() == 0) {
- Toast.makeText(this, R.string.active_flag_choose, Toast.LENGTH_LONG).show();
- return;
- }
- var selectedCountry = (KeyValueSpinnerItem) binding.upload.spCountries.getSelectedItem();
- upload.setCountry(selectedCountry.getValue());
- }
-
- SimpleDialogs.confirmOkCancel(this, station != null ? R.string.photo_upload : R.string.report_missing_station, (dialog, which) -> {
- binding.upload.progressBar.setVisibility(View.VISIBLE);
-
- var stationTitle = binding.upload.etStationTitle.getText().toString();
- var comment = binding.upload.etComment.getText().toString();
- upload.setTitle(stationTitle);
- upload.setComment(comment);
-
- try {
- stationTitle = URLEncoder.encode(binding.upload.etStationTitle.getText().toString(), String.valueOf(StandardCharsets.UTF_8));
- comment = URLEncoder.encode(comment, String.valueOf(StandardCharsets.UTF_8));
- } catch (UnsupportedEncodingException e) {
- Log.e(TAG, "Error encoding station title or comment", e);
- }
- upload.setActive(binding.upload.spActive.getSelectedItemPosition() == 1);
- baseApplication.getDbAdapter().updateUpload(upload);
-
- var file = mediaFile.exists() ? RequestBody.create(mediaFile, MediaType.parse(URLConnection.guessContentTypeFromName(mediaFile.getName()))) : RequestBody.create(new byte[]{}, MediaType.parse("application/octet-stream"));
- rsapiClient.photoUpload(bahnhofId, station != null ? station.getCountry() : upload.getCountry(),
- stationTitle, latitude, longitude, comment, upload.getActive(), file).enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- binding.upload.progressBar.setVisibility(View.GONE);
- InboxResponse inboxResponse;
- if (response.isSuccessful()) {
- inboxResponse = response.body();
- } else if (response.code() == 401) {
- onUnauthorized();
- return;
- } else {
- assert response.errorBody() != null;
- var gson = new Gson();
- inboxResponse = gson.fromJson(response.errorBody().charStream(), InboxResponse.class);
- }
-
- assert inboxResponse != null;
- upload.setRemoteId(inboxResponse.getId());
- upload.setInboxUrl(inboxResponse.getInboxUrl());
- upload.setUploadState(inboxResponse.getState().getUploadState());
- upload.setCrc32(inboxResponse.getCrc32());
- baseApplication.getDbAdapter().updateUpload(upload);
- if (inboxResponse.getState() == InboxResponse.InboxResponseState.ERROR) {
- SimpleDialogs.confirmOk(UploadActivity.this,
- getString(InboxResponse.InboxResponseState.ERROR.getMessageId(), inboxResponse.getMessage()));
- } else {
- SimpleDialogs.confirmOk(UploadActivity.this, inboxResponse.getState().getMessageId());
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- Log.e(TAG, "Error uploading photo", t);
- binding.upload.progressBar.setVisibility(View.GONE);
-
- SimpleDialogs.confirmOk(UploadActivity.this,
- getString(InboxResponse.InboxResponseState.ERROR.getMessageId(), t.getMessage()));
- fetchUploadStatus(upload); // try to get the upload state again
- }
- });
- });
- }
-
- private Intent createPhotoSendIntent() {
- var file = getStoredMediaFile(upload);
- if (file != null && file.canRead()) {
- var sendIntent = new Intent(Intent.ACTION_SEND);
- sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(UploadActivity.this,
- BuildConfig.APPLICATION_ID + ".fileprovider", file));
- return sendIntent;
- }
- return null;
- }
-
- /**
- * Fetch bitmap from device local location, if it exists, and set the photo view.
- */
- private void setLocalBitmap(Upload upload) {
- var localPhoto = checkForLocalPhoto(upload);
- if (localPhoto != null) {
- binding.upload.imageview.setImageBitmap(localPhoto);
- fetchUploadStatus(upload);
- }
- }
-
- private void fetchUploadStatus(Upload upload) {
- if (upload == null || upload.getRemoteId() == null) {
- return;
- }
- var stateQuery = new InboxStateQuery(
- upload.getRemoteId(),
- upload.getCountry(),
- upload.getStationId());
-
- rsapiClient.queryUploadState(List.of(stateQuery)).enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call> call, @NonNull Response> response) {
- if (response.isSuccessful()) {
- var stateQueries = response.body();
- if (stateQueries != null && !stateQueries.isEmpty()) {
- var stateQuery = stateQueries.get(0);
- binding.upload.uploadStatus.setText(getString(R.string.upload_state, getString(stateQuery.getState().getTextId())));
- binding.upload.uploadStatus.setTextColor(getResources().getColor(stateQuery.getState().getColorId(), null));
- binding.upload.uploadStatus.setVisibility(View.VISIBLE);
- upload.setUploadState(stateQuery.getState());
- upload.setRejectReason(stateQuery.getRejectedReason());
- upload.setCrc32(stateQuery.getCrc32());
- upload.setRemoteId(stateQuery.getId());
- baseApplication.getDbAdapter().updateUpload(upload);
- updateCrc32Checkbox();
- }
- } else if (response.code() == 401) {
- onUnauthorized();
- } else {
- Log.w(TAG, "Upload states not processable");
- }
- }
-
- @Override
- public void onFailure(@NonNull Call> call, @NonNull Throwable t) {
- Log.e(TAG, "Error retrieving upload state", t);
- }
- });
-
- }
-
- private void onUnauthorized() {
- baseApplication.setAccessToken(null);
- rsapiClient.clearToken();
- Toast.makeText(this, R.string.authorization_failed, Toast.LENGTH_LONG).show();
- startActivity(new Intent(this, MyDataActivity.class));
- finish();
- }
-
- private void updateCrc32Checkbox() {
- if (crc32 != null && upload != null) {
- var sameChecksum = crc32.equals(upload.getCrc32());
- binding.upload.cbChecksum.setVisibility(sameChecksum ? View.VISIBLE : View.GONE);
- }
- }
-
- /**
- * Check if there's a local photo file for this station.
- */
- @Nullable
- private Bitmap checkForLocalPhoto(Upload upload) {
- // show the image
- var localFile = getStoredMediaFile(upload);
- Log.d(TAG, "File: " + localFile);
- crc32 = null;
- if (localFile != null && localFile.canRead()) {
- Log.d(TAG, "FileGetPath: " + localFile.getPath());
- try (var cis = new CheckedInputStream(new FileInputStream(localFile), new CRC32())) {
- var scaledScreen = BitmapFactory.decodeStream(cis);
- crc32 = cis.getChecksum().getValue();
- Log.d(TAG, "img width " + scaledScreen.getWidth() + ", height " + scaledScreen.getHeight() + ", crc32 " + crc32);
- updateCrc32Checkbox();
- return scaledScreen;
- } catch (Exception e) {
- Log.e(TAG, String.format("Error reading media file for station %s", bahnhofId), e);
- }
- }
- return null;
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/CountryAdapter.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/CountryAdapter.java
deleted file mode 100644
index 21be4562..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/CountryAdapter.java
+++ /dev/null
@@ -1,123 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.db;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.CursorAdapter;
-
-import java.util.HashSet;
-import java.util.Set;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.BaseApplication;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ItemCountryBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.Constants;
-
-public class CountryAdapter extends CursorAdapter {
- private final LayoutInflater layoutInflater;
- private final String TAG = getClass().getSimpleName();
- private final Set selectedCountries;
-
- private final Context context;
-
- public CountryAdapter(Context context, Cursor c, int flags) {
- super(context, c, flags);
- layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- selectedCountries = new HashSet<>(BaseApplication.getInstance().getCountryCodes());
- this.context = context;
- }
-
- public void getView(int selectedPosition, View convertView, ViewGroup parent, Cursor cursor) {
- ItemCountryBinding binding;
- if (convertView == null) {
- binding = ItemCountryBinding.inflate(layoutInflater, parent, false);
- convertView = binding.getRoot();
-
- if (selectedPosition % 2 == 1) {
- convertView.setBackgroundResource(R.drawable.item_list_backgroundcolor);
- } else {
- convertView.setBackgroundResource(R.drawable.item_list_backgroundcolor2);
- }
-
- convertView.setTag(binding);
- } else {
- binding = (ItemCountryBinding) convertView.getTag();
- }
-
- var countryCode = cursor.getString(cursor.getColumnIndexOrThrow(Constants.COUNTRIES.COUNTRYSHORTCODE));
- binding.txtCountryShortCode.setText(countryCode);
- var countryName = getCountryName(countryCode, cursor.getString(cursor.getColumnIndexOrThrow(Constants.COUNTRIES.COUNTRYNAME)));
- binding.txtCountryName.setText(countryName);
-
- var newCountry = cursor.getString(1);
- Log.i(TAG, newCountry);
- if (selectedCountries.contains(newCountry)) {
- binding.checkCountry.setChecked(false);
- selectedCountries.remove(newCountry);
- } else {
- binding.checkCountry.setChecked(true);
- selectedCountries.add(newCountry);
- }
-
- }
-
- private String getCountryName(final String countryCode, final String defaultName) {
- int strId = context.getResources().getIdentifier("country_" + countryCode, "string", context.getPackageName());
- if (strId != 0) {
- return context.getString(strId);
- }
- return defaultName;
- }
-
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- var binding = ItemCountryBinding.inflate(layoutInflater, parent, false);
- var view = binding.getRoot();
- view.setTag(binding);
- return view;
- }
-
- @Override
- public void bindView(View view, Context context, Cursor cursor) {
- //If you want to have zebra lines color effect uncomment below code
- if (cursor.getPosition() % 2 == 1) {
- view.setBackgroundResource(R.drawable.item_list_backgroundcolor);
- } else {
- view.setBackgroundResource(R.drawable.item_list_backgroundcolor2);
- }
-
- var binding = (ItemCountryBinding) view.getTag();
-
- var countryCode = cursor.getString(cursor.getColumnIndexOrThrow(Constants.COUNTRIES.COUNTRYSHORTCODE));
- binding.txtCountryShortCode.setText(countryCode);
- var countryName = getCountryName(countryCode, cursor.getString(cursor.getColumnIndexOrThrow(Constants.COUNTRIES.COUNTRYNAME)));
- binding.txtCountryName.setText(countryName);
-
- var newCountry = cursor.getString(1);
- Log.i(TAG, newCountry);
- binding.checkCountry.setChecked(selectedCountries.contains(newCountry));
- binding.checkCountry.setOnClickListener(onStateChangedListener(binding.checkCountry, cursor.getPosition()));
- }
-
- private View.OnClickListener onStateChangedListener(CheckBox checkCountry, int position) {
- return v -> {
- var cursor = (Cursor) getItem(position);
- var country = cursor.getString(cursor.getColumnIndexOrThrow(Constants.COUNTRIES.COUNTRYSHORTCODE));
- if (checkCountry.isChecked()) {
- selectedCountries.add(country);
- } else {
- selectedCountries.remove(country);
- }
- };
- }
-
- public Set getSelectedCountries() {
- return selectedCountries;
- }
-
-}
-
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/DbAdapter.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/DbAdapter.java
deleted file mode 100644
index abcb3acc..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/DbAdapter.java
+++ /dev/null
@@ -1,780 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.db;
-
-import static java.util.stream.Collectors.joining;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.location.Location;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.text.Normalizer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.function.Predicate;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.InboxStateQuery;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PhotoStation;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PhotoStations;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.ProblemType;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.ProviderApp;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Statistic;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Upload;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.UploadState;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.Constants;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.StationFilter;
-
-public class DbAdapter {
-
- private static final String TAG = DbAdapter.class.getSimpleName();
-
- private static final String DATABASE_TABLE_STATIONS = "bahnhoefe";
- private static final String DATABASE_TABLE_COUNTRIES = "laender";
- private static final String DATABASE_TABLE_PROVIDER_APPS = "providerApps";
- private static final String DATABASE_TABLE_UPLOADS = "uploads";
- private static final String DATABASE_NAME = "bahnhoefe.db";
- private static final int DATABASE_VERSION = 22;
-
- private static final String CREATE_STATEMENT_STATIONS = "CREATE TABLE " + DATABASE_TABLE_STATIONS + " ("
- + Constants.STATIONS.ROWID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
- + Constants.STATIONS.COUNTRY + " TEXT, "
- + Constants.STATIONS.ID + " TEXT, "
- + Constants.STATIONS.TITLE + " TEXT, "
- + Constants.STATIONS.NORMALIZED_TITLE + " TEXT, "
- + Constants.STATIONS.LAT + " REAL, "
- + Constants.STATIONS.LON + " REAL, "
- + Constants.STATIONS.PHOTO_ID + " INTEGER, "
- + Constants.STATIONS.PHOTO_URL + " TEXT, "
- + Constants.STATIONS.PHOTOGRAPHER + " TEXT, "
- + Constants.STATIONS.PHOTOGRAPHER_URL + " TEXT, "
- + Constants.STATIONS.LICENSE + " TEXT, "
- + Constants.STATIONS.LICENSE_URL + " TEXT, "
- + Constants.STATIONS.DS100 + " TEXT, "
- + Constants.STATIONS.ACTIVE + " INTEGER, "
- + Constants.STATIONS.OUTDATED + " INTEGER)";
- private static final String CREATE_STATEMENT_STATIONS_IDX = "CREATE INDEX " + DATABASE_TABLE_STATIONS + "_IDX "
- + "ON " + DATABASE_TABLE_STATIONS + "(" + Constants.STATIONS.COUNTRY + ", " + Constants.STATIONS.ID + ")";
- private static final String CREATE_STATEMENT_COUNTRIES = "CREATE TABLE " + DATABASE_TABLE_COUNTRIES + " ("
- + Constants.COUNTRIES.ROWID_COUNTRIES + " INTEGER PRIMARY KEY AUTOINCREMENT, "
- + Constants.COUNTRIES.COUNTRYSHORTCODE + " TEXT, "
- + Constants.COUNTRIES.COUNTRYNAME + " TEXT, "
- + Constants.COUNTRIES.EMAIL + " TEXT, "
- + Constants.COUNTRIES.TIMETABLE_URL_TEMPLATE + " TEXT, "
- + Constants.COUNTRIES.OVERRIDE_LICENSE + " TEXT)";
- private static final String CREATE_STATEMENT_PROVIDER_APPS = "CREATE TABLE " + DATABASE_TABLE_PROVIDER_APPS + " ("
- + Constants.PROVIDER_APPS.COUNTRYSHORTCODE + " TEXT,"
- + Constants.PROVIDER_APPS.PA_TYPE + " TEXT,"
- + Constants.PROVIDER_APPS.PA_NAME + " TEXT, "
- + Constants.PROVIDER_APPS.PA_URL + " TEXT)";
- private static final String CREATE_STATEMENT_UPLOADS = "CREATE TABLE " + DATABASE_TABLE_UPLOADS + " ("
- + Constants.UPLOADS.ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
- + Constants.UPLOADS.STATION_ID + " TEXT, "
- + Constants.UPLOADS.COUNTRY + " TEXT, "
- + Constants.UPLOADS.REMOTE_ID + " INTEGER, "
- + Constants.UPLOADS.TITLE + " TEXT, "
- + Constants.UPLOADS.LAT + " REAL, "
- + Constants.UPLOADS.LON + " REAL, "
- + Constants.UPLOADS.COMMENT + " TEXT, "
- + Constants.UPLOADS.INBOX_URL + " TEXT, "
- + Constants.UPLOADS.PROBLEM_TYPE + " TEXT, "
- + Constants.UPLOADS.REJECTED_REASON + " TEXT, "
- + Constants.UPLOADS.UPLOAD_STATE + " TEXT, "
- + Constants.UPLOADS.CREATED_AT + " INTEGER, "
- + Constants.UPLOADS.ACTIVE + " INTEGER, "
- + Constants.UPLOADS.CRC32 + " INTEGER)";
-
- private static final String DROP_STATEMENT_STATIONS_IDX = "DROP INDEX IF EXISTS " + DATABASE_TABLE_STATIONS + "_IDX";
- private static final String DROP_STATEMENT_STATIONS = "DROP TABLE IF EXISTS " + DATABASE_TABLE_STATIONS;
- private static final String DROP_STATEMENT_COUNTRIES = "DROP TABLE IF EXISTS " + DATABASE_TABLE_COUNTRIES;
- private static final String DROP_STATEMENT_PROVIDER_APPS = "DROP TABLE IF EXISTS " + DATABASE_TABLE_PROVIDER_APPS;
-
- private final Context context;
- private DbOpenHelper dbHelper;
- private SQLiteDatabase db;
-
- public DbAdapter(Context context) {
- this.context = context;
- }
-
- public void open() {
- dbHelper = new DbOpenHelper(context);
- db = dbHelper.getWritableDatabase();
- }
-
- public void close() {
- db.close();
- dbHelper.close();
- }
-
- public void insertStations(PhotoStations photoStations, String countryCode) {
- db.beginTransaction();
- try {
- deleteStations(Set.of(countryCode));
- photoStations.getStations().forEach(station -> db.insert(DATABASE_TABLE_STATIONS, null, toContentValues(station, photoStations)));
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- private ContentValues toContentValues(PhotoStation station, PhotoStations photoStations) {
- var values = new ContentValues();
- values.put(Constants.STATIONS.ID, station.getId());
- values.put(Constants.STATIONS.COUNTRY, station.getCountry());
- values.put(Constants.STATIONS.TITLE, station.getTitle());
- values.put(Constants.STATIONS.NORMALIZED_TITLE, StringUtils.replaceChars(StringUtils.deleteWhitespace(StringUtils.stripAccents(Normalizer.normalize(station.getTitle(), Normalizer.Form.NFC))), "-_()", null));
- values.put(Constants.STATIONS.LAT, station.getLat());
- values.put(Constants.STATIONS.LON, station.getLon());
- values.put(Constants.STATIONS.DS100, station.getShortCode());
- values.put(Constants.STATIONS.ACTIVE, !station.getInactive());
- if (station.getPhotos().size() > 0) {
- var photo = station.getPhotos().get(0);
- values.put(Constants.STATIONS.PHOTO_ID, photo.getId());
- values.put(Constants.STATIONS.PHOTO_URL, photoStations.getPhotoBaseUrl() + photo.getPath());
- values.put(Constants.STATIONS.PHOTOGRAPHER, photo.getPhotographer());
- values.put(Constants.STATIONS.OUTDATED, photo.getOutdated());
- values.put(Constants.STATIONS.PHOTOGRAPHER_URL, photoStations.getPhotographerUrl(photo.getPhotographer()));
- values.put(Constants.STATIONS.LICENSE, photoStations.getLicenseName(photo.getLicense()));
- values.put(Constants.STATIONS.LICENSE_URL, photoStations.getLicenseUrl(photo.getLicense()));
- }
- return values;
- }
-
- public void insertCountries(List countries) {
- if (countries.isEmpty()) {
- return;
- }
- db.beginTransaction();
- try {
- deleteCountries();
- countries.forEach(this::insertCountry);
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- private void insertCountry(Country country) {
- db.insert(DATABASE_TABLE_COUNTRIES, null, toContentValues(country));
-
- country.getProviderApps().stream()
- .map(p -> toContentValues(country.getCode(), p))
- .forEach(values -> db.insert(DATABASE_TABLE_PROVIDER_APPS, null, values));
- }
-
- private ContentValues toContentValues(String countryCode, ProviderApp app) {
- var values = new ContentValues();
- values.put(Constants.PROVIDER_APPS.COUNTRYSHORTCODE, countryCode);
- values.put(Constants.PROVIDER_APPS.PA_TYPE, app.getType());
- values.put(Constants.PROVIDER_APPS.PA_NAME, app.getName());
- values.put(Constants.PROVIDER_APPS.PA_URL, app.getUrl());
- return values;
- }
-
- private ContentValues toContentValues(Country country) {
- var values = new ContentValues();
- values.put(Constants.COUNTRIES.COUNTRYSHORTCODE, country.getCode());
- values.put(Constants.COUNTRIES.COUNTRYNAME, country.getName());
- values.put(Constants.COUNTRIES.EMAIL, country.getEmail());
- values.put(Constants.COUNTRIES.TIMETABLE_URL_TEMPLATE, country.getTimetableUrlTemplate());
- values.put(Constants.COUNTRIES.OVERRIDE_LICENSE, country.getOverrideLicense());
- return values;
- }
-
- public Upload insertUpload(Upload upload) {
- upload.setId(db.insert(DATABASE_TABLE_UPLOADS, null, toContentValues(upload)));
- return upload;
- }
-
- public void deleteStations(Set countryCodes) {
- db.delete(DATABASE_TABLE_STATIONS, whereCountryCodeIn(countryCodes), null);
- }
-
- public void deleteCountries() {
- db.delete(DATABASE_TABLE_PROVIDER_APPS, null, null);
- db.delete(DATABASE_TABLE_COUNTRIES, null, null);
- }
-
- private String getStationOrderBy(boolean sortByDistance, Location myPos) {
- var orderBy = Constants.STATIONS.TITLE + " ASC";
-
- if (sortByDistance) {
- var fudge = Math.pow(Math.cos(Math.toRadians(myPos.getLatitude())), 2);
- orderBy = "((" + myPos.getLatitude() + " - " + Constants.STATIONS.LAT + ") * (" + myPos.getLatitude() + " - " + Constants.STATIONS.LAT + ") + " +
- "(" + myPos.getLongitude() + " - " + Constants.STATIONS.LON + ") * (" + myPos.getLongitude() + " - " + Constants.STATIONS.LON + ") * " + fudge + ")";
- }
-
- return orderBy;
- }
-
- public Cursor getCountryList() {
- var selectCountries = "SELECT " + Constants.COUNTRIES.ROWID_COUNTRIES + " AS " + Constants.CURSOR_ADAPTER_ID + ", " +
- Constants.COUNTRIES.COUNTRYSHORTCODE + ", " + Constants.COUNTRIES.COUNTRYNAME +
- " FROM " + DATABASE_TABLE_COUNTRIES + " ORDER BY " + Constants.COUNTRIES.COUNTRYNAME + " ASC";
- Log.d(TAG, selectCountries);
-
- var cursor = db.rawQuery(selectCountries, null);
-
- if (!cursor.moveToFirst()) {
- cursor.close();
- return null;
- }
- return cursor;
- }
-
-
- /**
- * Return a cursor on station ids where the station's title matches the given string
- *
- * @param search the search keyword
- * @param stationFilter if stations need to be filtered by photo available or not
- * @param countryCodes countries to search for
- * @param sortByDistance sort by distance or by alphabet
- * @param myPos current location
- * @return a Cursor representing the matching results
- */
- public Cursor getStationsListByKeyword(String search, StationFilter stationFilter, Set countryCodes, boolean sortByDistance, Location myPos) {
- var selectQuery = whereCountryCodeIn(countryCodes);
- var queryArgs = new ArrayList();
-
- if (StringUtils.isNotBlank(search)) {
- selectQuery += String.format(" AND %s LIKE ?", Constants.STATIONS.NORMALIZED_TITLE);
- queryArgs.add("%" + StringUtils.replaceChars(StringUtils.stripAccents(StringUtils.trimToEmpty(search)), " -_()", "%%%%%") + "%");
- }
-
- if (stationFilter.getNickname() != null) {
- selectQuery += " AND " + Constants.STATIONS.PHOTOGRAPHER + " = ?";
- queryArgs.add(stationFilter.getNickname());
- }
- if (stationFilter.hasPhoto() != null) {
- selectQuery += " AND " + Constants.STATIONS.PHOTO_URL + " IS " + (stationFilter.hasPhoto() ? "NOT" : "") + " NULL";
- }
- if (stationFilter.isActive() != null) {
- selectQuery += " AND " + Constants.STATIONS.ACTIVE + " = ?";
- queryArgs.add(stationFilter.isActive() ? "1" : "0");
- }
-
- Log.w(TAG, selectQuery);
-
- var cursor = db.query(DATABASE_TABLE_STATIONS,
- new String[]{
- Constants.STATIONS.ROWID + " AS " + Constants.CURSOR_ADAPTER_ID,
- Constants.STATIONS.ID,
- Constants.STATIONS.TITLE,
- Constants.STATIONS.PHOTO_URL,
- Constants.STATIONS.COUNTRY
- },
- selectQuery,
- queryArgs.toArray(new String[0]), null, null, getStationOrderBy(sortByDistance, myPos));
-
- if (!cursor.moveToFirst()) {
- Log.w(TAG, String.format("Query '%s' returned no result", search));
- cursor.close();
- return null;
- }
- return cursor;
- }
-
- private String whereCountryCodeIn(Set countryCodes) {
- return Constants.STATIONS.COUNTRY +
- " IN (" +
- countryCodes.stream().map(c -> "'" + c + "'").collect(joining(",")) +
- ")";
- }
-
- public Statistic getStatistic(String country) {
- try (var cursor = db.rawQuery("SELECT COUNT(*), COUNT(" + Constants.STATIONS.PHOTO_URL + "), COUNT(DISTINCT(" + Constants.STATIONS.PHOTOGRAPHER + ")) FROM " + DATABASE_TABLE_STATIONS + " WHERE " + Constants.STATIONS.COUNTRY + " = ?", new String[]{country})) {
- if (cursor.moveToNext()) {
- return new Statistic(cursor.getInt(0),
- cursor.getInt(1),
- cursor.getInt(0) - cursor.getInt(1),
- cursor.getInt(2));
- }
- }
- return null;
- }
-
- public String[] getPhotographerNicknames() {
- var photographers = new ArrayList();
- try (var cursor = db.rawQuery("SELECT distinct " + Constants.STATIONS.PHOTOGRAPHER + " FROM " + DATABASE_TABLE_STATIONS + " WHERE " + Constants.STATIONS.PHOTOGRAPHER + " IS NOT NULL ORDER BY " + Constants.STATIONS.PHOTOGRAPHER, null)) {
- while (cursor.moveToNext()) {
- photographers.add(cursor.getString(0));
- }
- }
- return photographers.toArray(new String[0]);
- }
-
- public int countStations(Set countryCodes) {
- try (var query = db.rawQuery("SELECT COUNT(*) FROM " + DATABASE_TABLE_STATIONS + " WHERE " + whereCountryCodeIn(countryCodes), null)) {
- if (query.moveToFirst()) {
- return query.getInt(0);
- }
- }
- return 0;
- }
-
- public void updateUpload(Upload upload) {
- db.beginTransaction();
- try {
- db.update(DATABASE_TABLE_UPLOADS, toContentValues(upload), Constants.UPLOADS.ID + " = ?", new String[]{String.valueOf(upload.getId())});
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- private ContentValues toContentValues(Upload upload) {
- var values = new ContentValues();
- values.put(Constants.UPLOADS.COMMENT, upload.getComment());
- values.put(Constants.UPLOADS.COUNTRY, upload.getCountry());
- values.put(Constants.UPLOADS.CREATED_AT, upload.getCreatedAt());
- values.put(Constants.UPLOADS.INBOX_URL, upload.getInboxUrl());
- values.put(Constants.UPLOADS.LAT, upload.getLat());
- values.put(Constants.UPLOADS.LON, upload.getLon());
- values.put(Constants.UPLOADS.PROBLEM_TYPE, upload.getProblemType() != null ? upload.getProblemType().name() : null);
- values.put(Constants.UPLOADS.REJECTED_REASON, upload.getRejectReason());
- values.put(Constants.UPLOADS.REMOTE_ID, upload.getRemoteId());
- values.put(Constants.UPLOADS.STATION_ID, upload.getStationId());
- values.put(Constants.UPLOADS.TITLE, upload.getTitle());
- values.put(Constants.UPLOADS.UPLOAD_STATE, upload.getUploadState().name());
- values.put(Constants.UPLOADS.ACTIVE, upload.getActive());
- values.put(Constants.UPLOADS.CRC32, upload.getCrc32());
- values.put(Constants.UPLOADS.REMOTE_ID, upload.getRemoteId());
- return values;
- }
-
- public List getPendingUploadsForStation(Station station) {
- var uploads = new ArrayList();
- try (var cursor = db.query(DATABASE_TABLE_UPLOADS, null, Constants.UPLOADS.COUNTRY + " = ? AND " + Constants.UPLOADS.STATION_ID + " = ? AND " + getPendingUploadWhereClause(),
- new String[]{station.getCountry(), station.getId()}, null, null, Constants.UPLOADS.CREATED_AT + " DESC")) {
- while (cursor.moveToNext()) {
- uploads.add(createUploadFromCursor(cursor));
- }
- }
-
- return uploads;
- }
-
- public Upload getPendingUploadForCoordinates(double lat, double lon) {
- try (var cursor = db.query(DATABASE_TABLE_UPLOADS, null, Constants.UPLOADS.LAT + " = ? AND " + Constants.UPLOADS.LON + " = ? AND " + getPendingUploadWhereClause(),
- new String[]{String.valueOf(lat), String.valueOf(lon)}, null, null, Constants.UPLOADS.CREATED_AT + " DESC")) {
- if (cursor.moveToFirst()) {
- return createUploadFromCursor(cursor);
- }
- }
-
- return null;
- }
-
- private String getUploadWhereClause(Predicate predicate) {
- return Constants.UPLOADS.UPLOAD_STATE + " IN (" +
- Arrays.stream(UploadState.values())
- .filter(predicate)
- .map(s -> "'" + s.name() + "'")
- .collect(joining(",")) +
- ')';
- }
-
- private String getPendingUploadWhereClause() {
- return getUploadWhereClause(UploadState::isPending);
- }
-
- private String getCompletedUploadWhereClause() {
- return getUploadWhereClause(s -> !s.isPending());
- }
-
- public Cursor getOutbox() {
- var queryBuilder = new SQLiteQueryBuilder();
- queryBuilder.setTables(DATABASE_TABLE_UPLOADS
- + " LEFT JOIN "
- + DATABASE_TABLE_STATIONS
- + " ON "
- + DATABASE_TABLE_STATIONS + "." + Constants.STATIONS.COUNTRY
- + " = "
- + DATABASE_TABLE_UPLOADS + "." + Constants.UPLOADS.COUNTRY
- + " AND "
- + DATABASE_TABLE_STATIONS + "." + Constants.STATIONS.ID
- + " = "
- + DATABASE_TABLE_UPLOADS + "." + Constants.UPLOADS.STATION_ID);
- return queryBuilder.query(db, new String[]{
- DATABASE_TABLE_UPLOADS + "." + Constants.UPLOADS.ID + " AS " + Constants.CURSOR_ADAPTER_ID,
- DATABASE_TABLE_UPLOADS + "." + Constants.UPLOADS.REMOTE_ID,
- DATABASE_TABLE_UPLOADS + "." + Constants.UPLOADS.COUNTRY,
- DATABASE_TABLE_UPLOADS + "." + Constants.UPLOADS.STATION_ID,
- DATABASE_TABLE_UPLOADS + "." + Constants.UPLOADS.TITLE,
- DATABASE_TABLE_UPLOADS + "." + Constants.UPLOADS.UPLOAD_STATE,
- DATABASE_TABLE_UPLOADS + "." + Constants.UPLOADS.PROBLEM_TYPE,
- DATABASE_TABLE_UPLOADS + "." + Constants.UPLOADS.COMMENT,
- DATABASE_TABLE_UPLOADS + "." + Constants.UPLOADS.REJECTED_REASON,
- DATABASE_TABLE_STATIONS + "." + Constants.UPLOADS.TITLE + " AS " + Constants.UPLOADS.JOIN_STATION_TITLE
- }, null, null, null, null, Constants.UPLOADS.CREATED_AT + " DESC");
- }
-
- public Upload getUploadById(long id) {
- try (var cursor = db.query(DATABASE_TABLE_UPLOADS, null, Constants.UPLOADS.ID + "=?",
- new String[]{String.valueOf(id)}, null, null, null)) {
- if (cursor.moveToFirst()) {
- return createUploadFromCursor(cursor);
- }
- }
-
- return null;
- }
-
- public void deleteUpload(long id) {
- db.delete(DATABASE_TABLE_UPLOADS, Constants.UPLOADS.ID + "=?", new String[]{String.valueOf(id)});
- }
-
- public List getPendingUploads(boolean withRemoteId) {
- var selection = getPendingUploadWhereClause();
- if (withRemoteId) {
- selection += " AND " + Constants.UPLOADS.REMOTE_ID + " IS NOT NULL";
- }
- var uploads = new ArrayList();
- try (var cursor = db.query(DATABASE_TABLE_UPLOADS, null, selection,
- null, null, null, null)) {
- while (cursor.moveToNext()) {
- uploads.add(createUploadFromCursor(cursor));
- }
- }
-
- return uploads;
- }
-
- public void updateUploadStates(List stateQueries) {
- db.beginTransaction();
- try {
- stateQueries.forEach(state -> db.update(DATABASE_TABLE_UPLOADS, toUploadStatesContentValues(state), Constants.UPLOADS.REMOTE_ID + " = ?", new String[]{String.valueOf(state.getId())}));
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- private ContentValues toUploadStatesContentValues(InboxStateQuery state) {
- var values = new ContentValues();
- values.put(Constants.UPLOADS.UPLOAD_STATE, state.getState().name());
- values.put(Constants.UPLOADS.REJECTED_REASON, state.getRejectedReason());
- values.put(Constants.UPLOADS.CRC32, state.getCrc32());
- return values;
- }
-
- public List getCompletedUploads() {
- var uploads = new ArrayList();
- try (var cursor = db.query(DATABASE_TABLE_UPLOADS, null,
- Constants.UPLOADS.REMOTE_ID + " IS NOT NULL AND " + getCompletedUploadWhereClause(),
- null, null, null, null)) {
- while (cursor.moveToNext()) {
- uploads.add(createUploadFromCursor(cursor));
- }
- }
-
- return uploads;
- }
-
- static class DbOpenHelper extends SQLiteOpenHelper {
-
- DbOpenHelper(Context context) {
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- Log.i(TAG, "Creating database");
- db.execSQL(CREATE_STATEMENT_STATIONS);
- db.execSQL(CREATE_STATEMENT_STATIONS_IDX);
- db.execSQL(CREATE_STATEMENT_COUNTRIES);
- db.execSQL(CREATE_STATEMENT_PROVIDER_APPS);
- db.execSQL(CREATE_STATEMENT_UPLOADS);
- Log.i(TAG, "Database structure created.");
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- Log.w(TAG, "Upgrade database from version" + oldVersion + " to " + newVersion);
-
- db.beginTransaction();
-
- if (oldVersion < 13) {
- // up to version 13 we dropped all tables and recreated them
- db.execSQL(DROP_STATEMENT_STATIONS_IDX);
- db.execSQL(DROP_STATEMENT_STATIONS);
- db.execSQL(DROP_STATEMENT_COUNTRIES);
- db.execSQL(DROP_STATEMENT_PROVIDER_APPS);
- onCreate(db);
- } else {
- // from now on we need to preserve user data and perform schema changes selectively
-
- if (oldVersion < 14) {
- db.execSQL(CREATE_STATEMENT_UPLOADS);
- }
-
- if (oldVersion < 15) {
- db.execSQL(DROP_STATEMENT_STATIONS_IDX);
- db.execSQL(CREATE_STATEMENT_STATIONS_IDX);
- }
-
- if (oldVersion < 16) {
- db.execSQL("ALTER TABLE " + DATABASE_TABLE_COUNTRIES + " ADD COLUMN " + Constants.COUNTRIES.OVERRIDE_LICENSE + " TEXT");
- }
-
- if (oldVersion < 17) {
- db.execSQL("ALTER TABLE " + DATABASE_TABLE_UPLOADS + " ADD COLUMN " + Constants.UPLOADS.ACTIVE + " INTEGER");
- }
-
- if (oldVersion < 18) {
- db.execSQL("ALTER TABLE " + DATABASE_TABLE_STATIONS + " ADD COLUMN " + Constants.STATIONS.NORMALIZED_TITLE + " TEXT");
- db.execSQL("UPDATE " + DATABASE_TABLE_STATIONS + " SET " + Constants.STATIONS.NORMALIZED_TITLE + " = " + Constants.STATIONS.TITLE);
- }
-
- if (oldVersion < 19) {
- db.execSQL("ALTER TABLE " + DATABASE_TABLE_UPLOADS + " ADD COLUMN " + Constants.UPLOADS.CRC32 + " INTEGER");
- }
-
- if (oldVersion < 20) {
- db.execSQL("ALTER TABLE " + DATABASE_TABLE_STATIONS + " ADD COLUMN " + Constants.STATIONS.OUTDATED + " INTEGER");
- }
-
- if (oldVersion < 21) {
- db.execSQL("ALTER TABLE " + DATABASE_TABLE_STATIONS + " ADD COLUMN " + Constants.STATIONS.PHOTO_ID + " INTEGER");
- }
-
- }
-
- db.setTransactionSuccessful();
- db.endTransaction();
- }
- }
-
- @NonNull
- private Station createStationFromCursor(@NonNull Cursor cursor) {
- return new Station(
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.COUNTRY)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.ID)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.TITLE)),
- cursor.getDouble(cursor.getColumnIndexOrThrow(Constants.STATIONS.LAT)),
- cursor.getDouble(cursor.getColumnIndexOrThrow(Constants.STATIONS.LON)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.DS100)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.PHOTO_URL)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.PHOTOGRAPHER)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.PHOTOGRAPHER_URL)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.LICENSE)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.LICENSE_URL)),
- Boolean.TRUE.equals(getBoolean(cursor, Constants.STATIONS.ACTIVE)),
- Boolean.TRUE.equals(getBoolean(cursor, Constants.STATIONS.OUTDATED)),
- cursor.getLong(cursor.getColumnIndexOrThrow(Constants.STATIONS.PHOTO_ID))
- );
- }
-
- @NonNull
- private Country createCountryFromCursor(@NonNull Cursor cursor) {
- return new Country(
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.COUNTRIES.COUNTRYSHORTCODE)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.COUNTRIES.COUNTRYNAME)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.COUNTRIES.EMAIL)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.COUNTRIES.TIMETABLE_URL_TEMPLATE)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.COUNTRIES.OVERRIDE_LICENSE)));
- }
-
- @NonNull
- private ProviderApp createProviderAppFromCursor(@NonNull Cursor cursor) {
- return new ProviderApp(
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.PROVIDER_APPS.PA_TYPE)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.PROVIDER_APPS.PA_NAME)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.PROVIDER_APPS.PA_URL)));
- }
-
- @NonNull
- private Upload createUploadFromCursor(@NonNull Cursor cursor) {
- var upload = new Upload(
- cursor.getLong(cursor.getColumnIndexOrThrow(Constants.UPLOADS.ID)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.COUNTRY)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.STATION_ID)),
- getLong(cursor, Constants.UPLOADS.REMOTE_ID),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.TITLE)),
- getDouble(cursor, Constants.UPLOADS.LAT),
- getDouble(cursor, Constants.UPLOADS.LON),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.COMMENT)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.INBOX_URL)),
- null,
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.REJECTED_REASON)),
- UploadState.UNKNOWN,
- cursor.getLong(cursor.getColumnIndexOrThrow(Constants.UPLOADS.CREATED_AT)),
- getBoolean(cursor, Constants.UPLOADS.ACTIVE),
- getLong(cursor, Constants.UPLOADS.CRC32));
-
- var problemType = cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.PROBLEM_TYPE));
- if (problemType != null) {
- upload.setProblemType(ProblemType.valueOf(problemType));
- }
- var uploadState = cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.UPLOAD_STATE));
- if (uploadState != null) {
- upload.setUploadState(UploadState.valueOf(uploadState));
- }
-
- return upload;
- }
-
- private Boolean getBoolean(Cursor cursor, String columnName) {
- if (!cursor.isNull(cursor.getColumnIndexOrThrow(columnName))) {
- return cursor.getInt(cursor.getColumnIndexOrThrow(columnName)) == 1;
- }
- return null;
- }
-
- private Double getDouble(final Cursor cursor, final String columnName) {
- if (!cursor.isNull(cursor.getColumnIndexOrThrow(columnName))) {
- return cursor.getDouble(cursor.getColumnIndexOrThrow(columnName));
- }
- return null;
- }
-
- private Long getLong(final Cursor cursor, final String columnName) {
- if (!cursor.isNull(cursor.getColumnIndexOrThrow(columnName))) {
- return cursor.getLong(cursor.getColumnIndexOrThrow(columnName));
- }
- return null;
- }
-
- public Station fetchStationByRowId(long id) {
- try (var cursor = db.query(DATABASE_TABLE_STATIONS, null, Constants.STATIONS.ROWID + "=?", new String[]{
- id + ""}, null, null, null)) {
- if (cursor.moveToFirst()) {
- return createStationFromCursor(cursor);
- }
- }
- return null;
- }
-
- public Station getStationByKey(String country, String id) {
- try (var cursor = db.query(DATABASE_TABLE_STATIONS, null, Constants.STATIONS.COUNTRY + "=? AND " + Constants.STATIONS.ID + "=?",
- new String[]{country, id}, null, null, null)) {
- if (cursor.moveToFirst()) {
- return createStationFromCursor(cursor);
- }
- }
- return null;
- }
-
- public Station getStationForUpload(Upload upload) {
- try (var cursor = db.query(DATABASE_TABLE_STATIONS, null, Constants.STATIONS.COUNTRY + "=? AND " + Constants.STATIONS.ID + "=?",
- new String[]{upload.getCountry(), upload.getStationId()}, null, null, null)) {
- if (cursor.moveToFirst()) {
- return createStationFromCursor(cursor);
- }
- }
- return null;
- }
-
- public Set fetchCountriesWithProviderApps(Set countryCodes) {
- var countryList = countryCodes.stream()
- .map(c -> "'" + c + "'")
- .collect(joining(","));
- var countries = new HashSet();
- try (var cursor = db.query(DATABASE_TABLE_COUNTRIES, null, Constants.COUNTRIES.COUNTRYSHORTCODE + " IN (" + countryList + ")",
- null, null, null, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- do {
- var country = createCountryFromCursor(cursor);
- countries.add(country);
- try (var cursorPa = db.query(DATABASE_TABLE_PROVIDER_APPS, null, Constants.PROVIDER_APPS.COUNTRYSHORTCODE + " = ?",
- new String[]{country.getCode()}, null, null, null)) {
- if (cursorPa != null && cursorPa.moveToFirst()) {
- do {
- country.getProviderApps().add(createProviderAppFromCursor(cursorPa));
- } while (cursorPa.moveToNext());
- }
- }
- } while (cursor.moveToNext());
- }
- }
-
- return countries;
- }
-
- public List getAllStations(StationFilter stationFilter, Set countryCodes) {
- var stationList = new ArrayList();
- var selectQuery = "SELECT * FROM " + DATABASE_TABLE_STATIONS + " WHERE " + whereCountryCodeIn(countryCodes);
- var queryArgs = new ArrayList();
- if (stationFilter.getNickname() != null) {
- selectQuery += " AND " + Constants.STATIONS.PHOTOGRAPHER + " = ?";
- queryArgs.add(stationFilter.getNickname());
- }
- if (stationFilter.hasPhoto() != null) {
- selectQuery += " AND " + Constants.STATIONS.PHOTO_URL + " IS " + (stationFilter.hasPhoto() ? "NOT" : "") + " NULL";
- }
- if (stationFilter.isActive() != null) {
- selectQuery += " AND " + Constants.STATIONS.ACTIVE + " = ?";
- queryArgs.add(stationFilter.isActive() ? "1" : "0");
- }
-
- try (var cursor = db.rawQuery(selectQuery, queryArgs.toArray(new String[]{}))) {
- if (cursor.moveToFirst()) {
- do {
- stationList.add(createStationFromCursor(cursor));
- } while (cursor.moveToNext());
- }
- }
-
- return stationList;
- }
-
- public List getStationByLatLngRectangle(double lat, double lng, StationFilter stationFilter) {
- var stationList = new ArrayList();
- // Select All Query with rectangle - might be later change with it
- var selectQuery = "SELECT * FROM " + DATABASE_TABLE_STATIONS + " WHERE " + Constants.STATIONS.LAT + " < " + (lat + 0.5) + " AND " + Constants.STATIONS.LAT + " > " + (lat - 0.5)
- + " AND " + Constants.STATIONS.LON + " < " + (lng + 0.5) + " AND " + Constants.STATIONS.LON + " > " + (lng - 0.5);
-
- var queryArgs = new ArrayList();
- if (stationFilter.getNickname() != null) {
- selectQuery += " AND " + Constants.STATIONS.PHOTOGRAPHER + " = ?";
- queryArgs.add(stationFilter.getNickname());
- }
- if (stationFilter.hasPhoto() != null) {
- selectQuery += " AND " + Constants.STATIONS.PHOTO_URL + " IS " + (stationFilter.hasPhoto() ? "NOT" : "") + " NULL";
- }
- if (stationFilter.isActive() != null) {
- selectQuery += " AND " + Constants.STATIONS.ACTIVE + " = ?";
- queryArgs.add(stationFilter.isActive() ? "1" : "0");
- }
-
- try (var cursor = db.rawQuery(selectQuery, queryArgs.toArray(new String[]{}))) {
- if (cursor.moveToFirst()) {
- do {
- stationList.add(createStationFromCursor(cursor));
- } while (cursor.moveToNext());
- }
- }
-
- return stationList;
- }
-
- public Set getAllCountries() {
- var countryList = new HashSet();
- var query = "SELECT * FROM " + DATABASE_TABLE_COUNTRIES;
-
- Log.d(TAG, query);
- try (var cursor = db.rawQuery(query, null)) {
- if (cursor.moveToFirst()) {
- do {
- countryList.add(createCountryFromCursor(cursor));
- } while (cursor.moveToNext());
- }
- }
-
- return countryList;
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/HighScoreAdapter.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/HighScoreAdapter.java
deleted file mode 100644
index cba4f629..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/HighScoreAdapter.java
+++ /dev/null
@@ -1,127 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.db;
-
-import android.app.Activity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.Filter;
-
-import androidx.annotation.NonNull;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ItemHighscoreBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.HighScoreItem;
-
-public class HighScoreAdapter extends ArrayAdapter {
- private final Activity context;
- private List highScore;
- private HighScoreFilter filter;
-
- public HighScoreAdapter(Activity context, List highScore) {
- super(context, R.layout.item_highscore, highScore);
- this.highScore = highScore;
- this.context = context;
- }
-
- @Override
- @NonNull
- public View getView(int position, View convertView, @NonNull ViewGroup parent) {
- var rowView = convertView;
- // reuse views
- ItemHighscoreBinding binding;
- if (rowView == null) {
- binding = ItemHighscoreBinding.inflate(context.getLayoutInflater(), parent, false);
- rowView = binding.getRoot();
- rowView.setTag(binding);
- } else {
- binding = (ItemHighscoreBinding) rowView.getTag();
- }
-
- var item = highScore.get(position);
- binding.highscoreName.setText(item.getName());
- binding.highscorePhotos.setText(String.valueOf(item.getPhotos()));
- binding.highscorePosition.setText(String.valueOf(item.getPosition()).concat("."));
-
- switch (item.getPosition()) {
- case 1:
- binding.highscoreAward.setImageResource(R.drawable.ic_crown_gold);
- binding.highscoreAward.setVisibility(View.VISIBLE);
- binding.highscorePosition.setVisibility(View.GONE);
- break;
- case 2:
- binding.highscoreAward.setImageResource(R.drawable.ic_crown_silver);
- binding.highscoreAward.setVisibility(View.VISIBLE);
- binding.highscorePosition.setVisibility(View.GONE);
- break;
- case 3:
- binding.highscoreAward.setImageResource(R.drawable.ic_crown_bronze);
- binding.highscoreAward.setVisibility(View.VISIBLE);
- binding.highscorePosition.setVisibility(View.GONE);
- break;
- default:
- binding.highscoreAward.setVisibility(View.GONE);
- binding.highscorePosition.setVisibility(View.VISIBLE);
- break;
- }
-
- if (position % 2 == 1) {
- rowView.setBackgroundResource(R.drawable.item_list_backgroundcolor);
- } else {
- rowView.setBackgroundResource(R.drawable.item_list_backgroundcolor2);
- }
-
- return rowView;
- }
-
- @NonNull
- @Override
- public Filter getFilter() {
- if (filter == null) {
- filter = new HighScoreFilter(highScore);
- }
- return filter;
- }
-
- private class HighScoreFilter extends Filter {
-
- private final List originalItems = new ArrayList<>();
-
- public HighScoreFilter(List originalItems) {
- this.originalItems.addAll(originalItems);
- }
-
- @Override
- protected FilterResults performFiltering(CharSequence constraint) {
- var filterResults = new FilterResults();
-
- if (constraint != null) {
- var search = constraint.toString().toLowerCase();
- var tempList = originalItems.stream()
- .filter(item -> item.getName().toLowerCase().contains(search))
- .collect(Collectors.toList());
-
- filterResults.values = tempList;
- filterResults.count = tempList.size();
- }
- return filterResults;
- }
-
- @SuppressWarnings("unchecked")
- @Override
- protected void publishResults(CharSequence contraint, FilterResults results) {
- highScore = (ArrayList) results.values;
- clear();
- addAll(highScore);
- if (results.count > 0) {
- notifyDataSetChanged();
- } else {
- notifyDataSetInvalidated();
- }
- }
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/InboxAdapter.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/InboxAdapter.java
deleted file mode 100644
index aa74a546..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/InboxAdapter.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.db;
-
-import android.app.Activity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-
-import androidx.annotation.NonNull;
-
-import java.util.List;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ItemInboxBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PublicInbox;
-
-public class InboxAdapter extends ArrayAdapter {
- private final Activity context;
- private final List publicInboxes;
-
- public InboxAdapter(Activity context, List publicInboxes) {
- super(context, R.layout.item_inbox, publicInboxes);
- this.publicInboxes = publicInboxes;
- this.context = context;
- }
-
- @Override
- @NonNull
- public View getView(int position, View convertView, @NonNull ViewGroup parent) {
- var rowView = convertView;
- // reuse views
- ItemInboxBinding binding;
- if (rowView == null) {
- binding = ItemInboxBinding.inflate(context.getLayoutInflater(), parent, false);
- rowView = binding.getRoot();
- rowView.setTag(binding);
- } else {
- binding = (ItemInboxBinding) rowView.getTag();
- }
-
- // fill data
- var item = publicInboxes.get(position);
- binding.txtStationName.setText(item.getTitle());
- if (item.getStationId() != null) {
- binding.txtStationId.setText(item.getCountryCode().concat(":").concat(item.getStationId()));
- } else {
- binding.txtStationId.setText(R.string.missing_station);
- }
- binding.txtCoordinates.setText(String.valueOf(item.getLat()).concat(",").concat(String.valueOf(item.getLon())));
-
- return rowView;
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/OutboxAdapter.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/OutboxAdapter.java
deleted file mode 100644
index 964c7a93..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/OutboxAdapter.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.db;
-
-import android.app.Activity;
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.BitmapFactory;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CursorAdapter;
-
-import androidx.appcompat.content.res.AppCompatResources;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ItemUploadBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.ProblemType;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.UploadState;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.Constants;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.FileUtils;
-
-public class OutboxAdapter extends CursorAdapter {
-
- private final Activity activity;
-
- public OutboxAdapter(Activity activity, Cursor uploadCursor) {
- super(activity, uploadCursor, 0);
- this.activity = activity;
- }
-
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- var binding = ItemUploadBinding.inflate(activity.getLayoutInflater(), parent, false);
- var view = binding.getRoot();
- view.setTag(binding);
- return view;
- }
-
- @Override
- public void bindView(View view, Context context, Cursor cursor) {
- var binding = (ItemUploadBinding) view.getTag();
- var id = cursor.getLong(cursor.getColumnIndexOrThrow(Constants.CURSOR_ADAPTER_ID));
- var remoteId = cursor.getLong(cursor.getColumnIndexOrThrow(Constants.UPLOADS.REMOTE_ID));
- var stationId = cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.STATION_ID));
- var uploadTitle = cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.TITLE));
- var stationTitle = cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.JOIN_STATION_TITLE));
- var problemType = cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.PROBLEM_TYPE));
- var uploadStateStr = cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.UPLOAD_STATE));
- var comment = cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.COMMENT));
- var rejectReason = cursor.getString(cursor.getColumnIndexOrThrow(Constants.UPLOADS.REJECTED_REASON));
-
- var uploadState = UploadState.valueOf(uploadStateStr);
- var textState = id + (remoteId > 0 ? "/" + remoteId : "") + ": " + context.getString(uploadState.getTextId());
- binding.txtState.setText(textState);
- binding.txtState.setTextColor(context.getResources().getColor(uploadState.getColorId(), null));
- binding.uploadPhoto.setImageBitmap(null);
- binding.uploadPhoto.setVisibility(View.GONE);
-
- if (problemType != null) {
- binding.uploadType.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_bullhorn_red_48px));
- binding.txtComment.setText(String.format("%s: %s", context.getText(ProblemType.valueOf(problemType).getMessageId()), comment));
- } else {
- binding.uploadType.setImageDrawable(AppCompatResources.getDrawable(context, stationId == null ? R.drawable.ic_station_red_24px : R.drawable.ic_photo_red_48px));
- binding.txtComment.setText(comment);
- var file = FileUtils.getStoredMediaFile(context, id);
- if (file != null) {
- var bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
- binding.uploadPhoto.setImageBitmap(bitmap);
- binding.uploadPhoto.setVisibility(View.VISIBLE);
- }
- }
- binding.txtComment.setVisibility(comment == null ? View.GONE : View.VISIBLE);
-
- binding.txtStationName.setText(uploadTitle != null ? uploadTitle : stationTitle);
- binding.txtRejectReason.setText(rejectReason);
- binding.txtRejectReason.setVisibility(rejectReason == null ? View.GONE : View.VISIBLE);
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/StationListAdapter.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/StationListAdapter.java
deleted file mode 100644
index 5ca474d2..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/db/StationListAdapter.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.db;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CursorAdapter;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ItemStationBinding;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.Constants;
-
-public class StationListAdapter extends CursorAdapter {
- private final LayoutInflater mInflater;
-
- public StationListAdapter(Context context, Cursor cursor, int flags) {
- super(context, cursor, flags);
- mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- }
-
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- var binding = ItemStationBinding.inflate(mInflater, parent, false);
- var view = binding.getRoot();
- view.setTag(binding);
- return view;
- }
-
- @Override
- public void bindView(View view, Context context, Cursor cursor) {
- //If you want to have zebra lines color effect uncomment below code
- if (cursor.getPosition() % 2 == 1) {
- view.setBackgroundResource(R.drawable.item_list_backgroundcolor);
- } else {
- view.setBackgroundResource(R.drawable.item_list_backgroundcolor2);
- }
-
- var binding = (ItemStationBinding) view.getTag();
- binding.txtState.setText(cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.COUNTRY)).concat(": ").concat(cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.ID))));
- binding.txtStationName.setText(cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.TITLE)));
- binding.hasPhoto.setVisibility(cursor.getString(cursor.getColumnIndexOrThrow(Constants.STATIONS.PHOTO_URL)) != null? View.VISIBLE : View.INVISIBLE);
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/AppInfoFragment.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/AppInfoFragment.java
deleted file mode 100644
index 6a35c9a0..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/AppInfoFragment.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs;
-
-import android.app.Dialog;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.text.Html;
-import android.text.method.LinkMovementMethod;
-import android.view.ContextThemeWrapper;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.BuildConfig;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-
-public class AppInfoFragment extends DialogFragment {
-
- @Override
- @NonNull
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- var textView = new TextView(getContext());
- textView.setLinksClickable(true);
- textView.setTextSize((float) 18);
- textView.setPadding(50, 50, 50, 50);
- textView.setMovementMethod(LinkMovementMethod.getInstance());
- textView.setText(Html.fromHtml(getString(R.string.app_info_text, BuildConfig.VERSION_NAME),Html.FROM_HTML_MODE_COMPACT));
- textView.setLinkTextColor(Color.parseColor("#c71c4d"));
-
- return new AlertDialog.Builder(new ContextThemeWrapper(getActivity(), R.style.AlertDialogCustom))
- .setIcon(R.mipmap.ic_launcher)
- .setTitle(R.string.app_info_title)
- .setPositiveButton(R.string.app_info_ok, (dialog, id) -> {
- // noop, just close dialog
- })
- .setView(textView)
- .create();
- }
-
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/MapInfoFragment.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/MapInfoFragment.java
deleted file mode 100644
index 0461260f..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/MapInfoFragment.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs;
-
-import android.app.Dialog;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.text.method.LinkMovementMethod;
-import android.view.ContextThemeWrapper;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-
-public class MapInfoFragment extends DialogFragment {
-
-
- @Override
- @NonNull
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- var textView = new TextView(getContext());
- textView.setTextSize((float) 18);
- textView.setPadding(50, 50, 50, 50);
- textView.setMovementMethod(LinkMovementMethod.getInstance());
- textView.setText(R.string.map_info_text);
- textView.setLinkTextColor(Color.parseColor("#c71c4d"));
-
- return new AlertDialog.Builder(new ContextThemeWrapper(getActivity(), R.style.AlertDialogCustom))
- .setIcon(R.mipmap.ic_launcher)
- .setTitle(R.string.map_info_title)
- .setPositiveButton(R.string.app_info_ok, (dialog, id) -> {
- // noop, just close dialog
- })
- .setView(textView)
- .create();
- }
-
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/SimpleDialogs.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/SimpleDialogs.java
deleted file mode 100644
index 677af6a3..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/SimpleDialogs.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.view.ContextThemeWrapper;
-import android.view.LayoutInflater;
-import android.widget.ArrayAdapter;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.PromptBinding;
-
-public class SimpleDialogs {
-
- private SimpleDialogs() {
- }
-
- public static void confirmOk(Context context, int message) {
- confirmOk(context, message, null);
- }
-
- public static void confirmOk(Context context, int message, DialogInterface.OnClickListener listener) {
- new AlertDialog.Builder(new ContextThemeWrapper(context, R.style.AlertDialogCustom))
- .setIcon(R.mipmap.ic_launcher)
- .setTitle(R.string.app_name)
- .setMessage(message)
- .setNeutralButton(R.string.button_ok_text, listener).create().show();
- }
-
- public static void confirmOk(Context context, CharSequence message) {
- confirmOk(context, message, null);
- }
-
- public static void confirmOk(Context context, CharSequence message, DialogInterface.OnClickListener listener) {
- new AlertDialog.Builder(new ContextThemeWrapper(context, R.style.AlertDialogCustom))
- .setIcon(R.mipmap.ic_launcher)
- .setTitle(R.string.app_name)
- .setMessage(message)
- .setNeutralButton(R.string.button_ok_text, listener).create().show();
- }
-
- public static void confirmOkCancel(Context context, int message, DialogInterface.OnClickListener listener) {
- new AlertDialog.Builder(new ContextThemeWrapper(context, R.style.AlertDialogCustom))
- .setIcon(R.mipmap.ic_launcher)
- .setTitle(R.string.app_name)
- .setMessage(message)
- .setPositiveButton(R.string.button_ok_text, listener)
- .setNegativeButton(R.string.button_cancel_text, null)
- .create().show();
- }
-
- public static void confirmOkCancel(Context context, String message, DialogInterface.OnClickListener listener) {
- new AlertDialog.Builder(new ContextThemeWrapper(context, R.style.AlertDialogCustom))
- .setIcon(R.mipmap.ic_launcher)
- .setTitle(R.string.app_name)
- .setMessage(message)
- .setPositiveButton(R.string.button_ok_text, listener)
- .setNegativeButton(R.string.button_cancel_text, null)
- .create().show();
- }
-
- public static void simpleSelect(Context context, CharSequence message, CharSequence[] items, DialogInterface.OnClickListener listener) {
- new AlertDialog.Builder(new ContextThemeWrapper(context, R.style.AlertDialogCustom))
- .setIcon(R.mipmap.ic_launcher)
- .setTitle(message)
- .setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, 0, items), listener)
- .create().show();
- }
-
- public static void prompt(Context context, int message, int inputType, int hint, String text, PromptListener listener) {
- var binding = PromptBinding.inflate(LayoutInflater.from(context));
- if (text != null) {
- binding.etPrompt.setText(text);
- }
- binding.etPrompt.setHint(hint);
- binding.etPrompt.setInputType(inputType);
-
- var alertDialog = new androidx.appcompat.app.AlertDialog.Builder(new ContextThemeWrapper(context, R.style.AlertDialogCustom))
- .setTitle(message)
- .setView(binding.getRoot())
- .setIcon(R.mipmap.ic_launcher)
- .setPositiveButton(android.R.string.ok, null)
- .setNegativeButton(android.R.string.cancel, (dialog, id1) -> dialog.cancel())
- .create();
-
- alertDialog.show();
- alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
- alertDialog.dismiss();
- listener.prompt(binding.etPrompt.getText().toString());
- });
- }
-
- public interface PromptListener {
- void prompt(String prompt);
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/StationFilterBar.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/StationFilterBar.java
deleted file mode 100644
index 3bf118ca..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/dialogs/StationFilterBar.java
+++ /dev/null
@@ -1,308 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.InsetDrawable;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.ContextThemeWrapper;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.Toast;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.content.res.AppCompatResources;
-import androidx.appcompat.view.menu.MenuBuilder;
-import androidx.appcompat.widget.PopupMenu;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.drawable.DrawableCompat;
-
-import com.google.android.material.chip.Chip;
-
-import java.util.stream.IntStream;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.BaseApplication;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.CountryActivity;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.StationFilter;
-
-public class StationFilterBar extends LinearLayout {
-
- private static final String TAG = StationFilterBar.class.getSimpleName();
-
- private final Chip toggleSort;
- private final Chip photoFilter;
- private final Chip activeFilter;
- private final Chip nicknameFilter;
- private final Chip countrySelection;
- private OnChangeListener listener;
- private BaseApplication baseApplication;
- private Activity activity;
-
- public StationFilterBar(Context context) {
- this(context, null);
- }
-
- public StationFilterBar(Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public StationFilterBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public StationFilterBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
-
- LayoutInflater.from(context).inflate(R.layout.station_filter_bar, this);
-
- toggleSort = findViewById(R.id.toggleSort);
- toggleSort.setOnClickListener(this::showSortMenu);
-
- photoFilter = findViewById(R.id.photoFilter);
- photoFilter.setOnClickListener(this::showPhotoFilter);
-
- activeFilter = findViewById(R.id.activeFilter);
- activeFilter.setOnClickListener(this::showActiveFilter);
-
- nicknameFilter = findViewById(R.id.nicknameFilter);
- nicknameFilter.setOnClickListener(this::selectNicknameFilter);
-
- countrySelection = findViewById(R.id.countrySelection);
- countrySelection.setOnClickListener(this::selectCountry);
- }
-
- private void setCloseIcon(final Chip chip, final int icon) {
- chip.setCloseIcon(AppCompatResources.getDrawable(this.baseApplication, icon));
- }
-
- private void setChipStatus(Chip chip, int iconRes, boolean active, int textRes) {
- setChipStatus(chip, iconRes, active, baseApplication.getString(textRes));
- }
-
- private void setChipStatus(Chip chip, int iconRes, boolean active, String text) {
- if (iconRes != 0) {
- chip.setChipIcon(getTintedDrawable(this.baseApplication, iconRes, getChipForegroundColor(active)));
- } else {
- chip.setChipIcon(null);
- }
- chip.setChipBackgroundColorResource(active ? R.color.colorPrimary : R.color.fullTransparent);
- chip.setTextColor(getChipForegroundColor(active));
- chip.setCloseIconTintResource(getChipForegroundColorRes(active));
- chip.setChipStrokeColorResource(active ? R.color.colorPrimary : R.color.chipForeground);
- chip.setText(text);
- chip.setTextEndPadding(0);
- if (TextUtils.isEmpty(text)) {
- chip.setTextStartPadding(0);
- } else {
- chip.setTextStartPadding(activity.getResources().getDimension(R.dimen.chip_textStartPadding));
- }
- }
-
- private Drawable getTintedDrawable(Context context, int imageId, int color) {
- if (imageId > 0) {
- var unwrappedDrawable = ContextCompat.getDrawable(context, imageId);
- return getTintedDrawable(unwrappedDrawable, color);
- }
- return null;
- }
-
- @Nullable
- private static Drawable getTintedDrawable(Drawable unwrappedDrawable, int color) {
- if (unwrappedDrawable != null) {
- var wrappedDrawable = DrawableCompat.wrap(unwrappedDrawable);
- DrawableCompat.setTint(wrappedDrawable, color);
- return wrappedDrawable;
- }
- return null;
- }
-
- private int getChipForegroundColor(final boolean active) {
- return this.baseApplication.getColor(getChipForegroundColorRes(active));
- }
-
- private int getChipForegroundColorRes(final boolean active) {
- return active ? R.color.colorOnPrimary : R.color.chipForeground;
- }
-
- public void init(BaseApplication baseApplication, Activity activity) {
- this.baseApplication = baseApplication;
- this.activity = activity;
- if (activity instanceof OnChangeListener onChangeListener) {
- listener = onChangeListener;
- }
- var stationFilter = baseApplication.getStationFilter();
-
- setChipStatus(photoFilter, stationFilter.getPhotoIcon(), stationFilter.isPhotoFilterActive(), R.string.no_text);
- setChipStatus(nicknameFilter, stationFilter.getNicknameIcon(), stationFilter.isNicknameFilterActive(), stationFilter.getNicknameText(this.baseApplication));
- setChipStatus(activeFilter, stationFilter.getActiveIcon(), stationFilter.isActiveFilterActive(), stationFilter.getActiveText());
- setChipStatus(countrySelection, R.drawable.ic_countries_active_24px, true, getCountryText(baseApplication));
-
- setSortOrder(baseApplication.getSortByDistance());
- }
-
- private static String getCountryText(final BaseApplication baseApplication) {
- return String.join(",", baseApplication.getCountryCodes());
- }
-
- private void showActiveFilter(View v) {
- var popup = new PopupMenu(activity, v);
- popup.getMenuInflater().inflate(R.menu.active_filter, popup.getMenu());
-
- popup.setOnMenuItemClickListener(menuItem -> {
- var stationFilter = baseApplication.getStationFilter();
- if (menuItem.getItemId() == R.id.active_filter_active) {
- stationFilter.setActive(Boolean.TRUE);
- } else if (menuItem.getItemId() == R.id.active_filter_inactive) {
- stationFilter.setActive(Boolean.FALSE);
- } else {
- stationFilter.setActive(null);
- }
- setChipStatus(activeFilter, stationFilter.getActiveIcon(), stationFilter.isActiveFilterActive(), R.string.no_text);
- updateStationFilter(stationFilter);
- return false;
- });
-
- setPopupMenuIcons(popup);
- popup.setOnDismissListener(menu -> setCloseIcon(activeFilter, R.drawable.ic_baseline_arrow_drop_up_24));
- popup.show();
- setCloseIcon(activeFilter, R.drawable.ic_baseline_arrow_drop_down_24);
- }
-
- private void showPhotoFilter(View v) {
- var popup = new PopupMenu(activity, v);
- popup.getMenuInflater().inflate(R.menu.photo_filter, popup.getMenu());
-
- popup.setOnMenuItemClickListener(menuItem -> {
- var stationFilter = baseApplication.getStationFilter();
- if (menuItem.getItemId() == R.id.photo_filter_has_photo) {
- stationFilter.setPhoto(Boolean.TRUE);
- } else if (menuItem.getItemId() == R.id.photo_filter_without_photo) {
- stationFilter.setPhoto(Boolean.FALSE);
- } else {
- stationFilter.setPhoto(null);
- }
- setChipStatus(photoFilter, stationFilter.getPhotoIcon(), stationFilter.isPhotoFilterActive(), R.string.no_text);
- updateStationFilter(stationFilter);
- return false;
- });
-
- setPopupMenuIcons(popup);
- popup.setOnDismissListener(menu -> setCloseIcon(photoFilter, R.drawable.ic_baseline_arrow_drop_up_24));
- popup.show();
- setCloseIcon(photoFilter, R.drawable.ic_baseline_arrow_drop_down_24);
- }
-
- public void selectCountry(View view) {
- getContext().startActivity(new Intent(getContext(), CountryActivity.class));
- }
-
- private void showSortMenu(View v) {
- var popup = new PopupMenu(activity, v);
- popup.getMenuInflater().inflate(R.menu.sort_order, popup.getMenu());
-
- popup.setOnMenuItemClickListener(menuItem -> {
- boolean sortByDistance = menuItem.getItemId() == R.id.sort_order_by_distance;
- setSortOrder(sortByDistance);
- baseApplication.setSortByDistance(sortByDistance);
- if (listener != null) {
- listener.sortOrderChanged(sortByDistance);
- }
- return false;
- });
-
- setPopupMenuIcons(popup);
- popup.setOnDismissListener(menu -> setCloseIcon(toggleSort, R.drawable.ic_baseline_arrow_drop_up_24));
- popup.show();
- setCloseIcon(toggleSort, R.drawable.ic_baseline_arrow_drop_down_24);
- }
-
- @SuppressLint("RestrictedApi")
- private void setPopupMenuIcons(final PopupMenu popup) {
- try {
- if (popup.getMenu() instanceof MenuBuilder menuBuilder) {
- menuBuilder.setOptionalIconsVisible(true);
- for (var item : menuBuilder.getVisibleItems()) {
- var iconMarginPx =
- TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, getResources().getDisplayMetrics());
- if (item.getIcon() != null) {
- InsetDrawable icon;
- icon = new InsetDrawable(item.getIcon(), iconMarginPx, 0, iconMarginPx, 0);
- icon.setTint(getResources().getColor(R.color.colorSurface, null));
- item.setIcon(icon);
- }
- }
- }
- } catch (Exception e) {
- Log.w(TAG, "Error setting popupMenuIcons: ", e);
- }
- }
-
- public void setSortOrder(boolean sortByDistance) {
- setChipStatus(toggleSort, sortByDistance ? R.drawable.ic_sort_by_distance_active_24px : R.drawable.ic_sort_by_alpha_active_24px, true, R.string.no_text);
- }
-
- public void selectNicknameFilter(View view) {
- var nicknames = baseApplication.getDbAdapter().getPhotographerNicknames();
- var stationFilter = baseApplication.getStationFilter();
- if (nicknames.length == 0) {
- Toast.makeText(getContext(), getContext().getString(R.string.no_nicknames_found), Toast.LENGTH_LONG).show();
- return;
- }
- int selectedNickname = IntStream.range(0, nicknames.length)
- .filter(i -> nicknames[i].equals(stationFilter.getNickname()))
- .findFirst().orElse(-1);
-
- new AlertDialog.Builder(new ContextThemeWrapper(getContext(), R.style.AlertDialogCustom))
- .setIcon(R.mipmap.ic_launcher)
- .setTitle(R.string.select_nickname)
- .setSingleChoiceItems(nicknames, selectedNickname, null)
- .setPositiveButton(R.string.button_ok_text, (dialog, whichButton) -> {
- dialog.dismiss();
- int selectedPosition = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
- if (selectedPosition >= 0 && nicknames.length > selectedPosition) {
- stationFilter.setNickname(nicknames[selectedPosition]);
- setChipStatus(nicknameFilter, stationFilter.getNicknameIcon(), stationFilter.isNicknameFilterActive(), stationFilter.getNicknameText(baseApplication));
- updateStationFilter(stationFilter);
- }
- })
- .setNeutralButton(R.string.button_remove_text, (dialog, whichButton) -> {
- dialog.dismiss();
- stationFilter.setNickname(null);
- setChipStatus(nicknameFilter, stationFilter.getNicknameIcon(), stationFilter.isNicknameFilterActive(), stationFilter.getNicknameText(baseApplication));
- updateStationFilter(stationFilter);
- })
- .setNegativeButton(R.string.button_myself_text, (dialog, whichButton) -> {
- dialog.dismiss();
- stationFilter.setNickname(baseApplication.getNickname());
- setChipStatus(nicknameFilter, stationFilter.getNicknameIcon(), stationFilter.isNicknameFilterActive(), stationFilter.getNicknameText(baseApplication));
- updateStationFilter(stationFilter);
- })
- .create().show();
- }
-
- private void updateStationFilter(StationFilter stationFilter) {
- baseApplication.setStationFilter(stationFilter);
- if (listener != null) {
- listener.stationFilterChanged(stationFilter);
- }
- }
-
- public void setSortOrderEnabled(boolean enabled) {
- toggleSort.setVisibility(enabled ? VISIBLE : GONE);
- }
-
- public interface OnChangeListener {
- void stationFilterChanged(StationFilter stationFilter);
-
- void sortOrderChanged(boolean sortByDistance);
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/Cluster.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/Cluster.java
deleted file mode 100644
index 0e593d99..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/Cluster.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright 2009 Huan Erdao
- * Copyright 2014 Martin Vennekamp
- * Copyright 2015 mapsforge.org
- *
- * This program is free software: you can redistribute it and/or modify it under the
- * terms of the GNU Lesser General Public License as published by the Free Software
- * Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
- * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License along with
- * this program. If not, see .
- */
-package de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge;
-
-import org.mapsforge.core.model.LatLong;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Cluster class.
- * contains single marker object(ClusterMarker). mostly wraps methods in ClusterMarker.
- */
-public class Cluster {
- /**
- * GeoClusterer object
- */
- private ClusterManager clusterManager;
- /**
- * Center of cluster
- */
- private LatLong center;
- /**
- * List of GeoItem within cluster
- */
- private final List items = Collections.synchronizedList(new ArrayList<>());
- /**
- * ClusterMarker object
- */
- private ClusterMarker clusterMarker;
-
- /**
- * @param clusterManager ClusterManager object.
- */
- public Cluster(ClusterManager clusterManager, T item) {
- this.clusterManager = clusterManager;
- this.clusterMarker = new ClusterMarker<>(this);
- addItem(item);
- }
-
- public ClusterManager getClusterManager() {
- return clusterManager;
- }
-
- public String getTitle() {
- if (getItems().size() == 1) {
- return getItems().get(0).getTitle();
- }
- synchronized (items) {
- return items.stream().filter(GeoItem::hasPhoto).count() + "/" + getItems().size();
- }
- }
-
- /**
- * add item to cluster object
- *
- * @param item GeoItem object to be added.
- */
- public synchronized void addItem(T item) {
- synchronized (items) {
- items.add(item);
- }
- if (center == null) {
- center = item.getLatLong();
- } else {
- // computing the centroid
- double lat = 0, lon = 0;
- int n = 0;
- synchronized (items) {
- for (T object : items) {
- if (object == null) {
- throw new NullPointerException("object == null");
- }
- if (object.getLatLong() == null) {
- throw new NullPointerException("object.getLatLong() == null");
- }
- lat += object.getLatLong().latitude;
- lon += object.getLatLong().longitude;
- n++;
- }
- }
- center = new LatLong(lat / n, lon / n);
- }
- }
-
- /**
- * get center of the cluster.
- *
- * @return center of the cluster in LatLong.
- */
- public LatLong getLocation() {
- return center;
- }
-
- /**
- * get list of GeoItem.
- *
- * @return list of GeoItem within cluster.
- */
- public synchronized List getItems() {
- return items;
- }
-
- /**
- * clears cluster object and removes the cluster from the layers collection.
- */
- public void clear() {
- if (clusterMarker != null) {
- var mapOverlays = clusterManager.getMapView().getLayerManager().getLayers();
- if (mapOverlays.contains(clusterMarker)) {
- mapOverlays.remove(clusterMarker);
- }
- clusterManager = null;
- clusterMarker = null;
- }
- synchronized (items) {
- items.clear();
- }
- }
-
- /**
- * add the ClusterMarker to the Layers if is within Viewport, otherwise remove.
- */
- public void redraw() {
- var mapOverlays = clusterManager.getMapView().getLayerManager().getLayers();
- if (clusterMarker != null && center != null
- && clusterManager.getCurBounds() != null
- && !clusterManager.getCurBounds().contains(center)
- && mapOverlays.contains(clusterMarker)) {
- mapOverlays.remove(clusterMarker);
- return;
- }
- if (clusterMarker != null
- && mapOverlays.size() > 0
- && !mapOverlays.contains(clusterMarker)
- && !clusterManager.isClustering) {
- mapOverlays.add(1, clusterMarker);
- }
- }
-
- public int getSize() {
- return items.size();
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/ClusterManager.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/ClusterManager.java
deleted file mode 100644
index fb0ce88c..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/ClusterManager.java
+++ /dev/null
@@ -1,408 +0,0 @@
-/*
- * Copyright 2009 Huan Erdao
- * Copyright 2014 Martin Vennekamp
- * Copyright 2015 mapsforge.org
- *
- * This program is free software: you can redistribute it and/or modify it under the
- * terms of the GNU Lesser General Public License as published by the Free Software
- * Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
- * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License along with
- * this program. If not, see .
- */
-package de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge;
-
-import android.util.Log;
-import android.widget.Toast;
-
-import org.mapsforge.core.model.BoundingBox;
-import org.mapsforge.core.model.LatLong;
-import org.mapsforge.map.model.DisplayModel;
-import org.mapsforge.map.model.common.Observer;
-import org.mapsforge.map.view.MapView;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Class for Clustering geotagged content. this clustering came from
- * "markerclusterer" which is available as opensource at
- * https://code.google.com/p/android-maps-extensions/, resp
- * https://github.com/googlemaps/android-maps-utils this is android ported
- * version with modification to fit to the application refer also to other
- * implementations:
- * https://code.google.com/p/osmbonuspack/source/browse/#svn%2Ftrunk
- * %2FOSMBonusPack%2Fsrc%2Forg%2Fosmdroid%2Fbonuspack%2Fclustering
- * http://developer.nokia.com/community/wiki/
- * Map_Marker_Clustering_Strategies_for_the_Maps_API_for_Java_ME
- *
- * based on http://ge.tt/7Zq63CH/v/1
- *
- * Should be added as Observer on Mapsforge frameBufferModel.
- */
-public class ClusterManager implements Observer, TapHandler {
- private static final String TAG = ClusterManager.class.getSimpleName();
- protected static final int MIN_CLUSTER_SIZE = 5;
- /**
- * A 'Toast' to display information, intended to show information on {@link ClusterMarker}
- * with more than one {@link GeoItem} (while Marker with a single GeoItem should have their
- * own OnClick functions)
- */
- protected static Toast toast;
- /**
- * grid size for Clustering(dip).
- */
- protected final float GRIDSIZE = 60 * DisplayModel.getDeviceScaleFactor();
- /**
- * MapView object.
- */
- protected final MapView mapView;
-
- /**
- * The maximum (highest) zoom level for clustering the items.,
- */
- protected final byte maxClusteringZoom;
- /**
- * Lock for the re-Clustering of the items.
- */
- public boolean isClustering = false;
- /**
- * MarkerBitmap object for marker icons, uses Static access.
- */
- protected final List markerIconBmps;
-
- /**
- * The current BoundingBox of the viewport.
- */
- protected BoundingBox currBoundingBox;
- /**
- * GeoItem ArrayList object that are out of viewport and need to be
- * clustered on panning.
- */
- protected final List leftItems = Collections.synchronizedList(new ArrayList<>());
- /**
- * Clustered object list.
- */
- protected final List> clusters = Collections.synchronizedList(new ArrayList<>());
- /**
- * Handles on click on markers
- */
- protected final TapHandler tapHandler;
-
- /**
- * saves the actual ZoolLevel of the MapView
- */
- private double oldZoolLevel;
- /**
- * saves the actual Center as LatLong of the MapViewPosition
- */
- private LatLong oldCenterLatLong;
- private ClusterTask clusterTask;
-
- /**
- * @param mapView The Mapview in which the markers are shoen
- * @param markerBitmaps a list of well formed {@link MarkerBitmap}
- * @param maxClusteringZoom The maximum zoom level, beyond this level no clustering is performed.
- */
- public ClusterManager(MapView mapView,
- List markerBitmaps, byte maxClusteringZoom, TapHandler tapHandler) {
- this.mapView = mapView;
-
- // set to impossible values to trigger clustering at first onChange
- oldZoolLevel = -1;
- oldCenterLatLong = new LatLong(-90.0, -180.0);
- this.markerIconBmps = markerBitmaps;
-
- this.maxClusteringZoom = maxClusteringZoom;
- this.tapHandler = tapHandler;
- }
-
- /**
- * You might like to set the Toast from external, in order to make sure that only a single Toast
- * is showing up.
- *
- * @param toast A 'Toast' to display information, intended to show information on {@link ClusterMarker}
- */
- public static void setToast(Toast toast) {
- ClusterManager.toast = toast;
- }
-
- public MapView getMapView() {
- return mapView;
- }
-
- public synchronized List getAllItems() {
- List rtnList = Collections.synchronizedList(new ArrayList<>());
- synchronized (leftItems) {
- rtnList.addAll(leftItems);
- }
- synchronized (clusters) {
- clusters.stream().map(Cluster::getItems).forEach(rtnList::addAll);
- }
- return rtnList;
- }
-
- /**
- * add item and do isClustering. NOTE: this method will not redraw screen.
- * after adding all items, you must call redraw() method.
- *
- * @param item GeoItem to be clustered.
- */
- public synchronized void addItem(T item) {
- if (mapView.getWidth() == 0 || !isItemInViewport(item)) {
- synchronized (leftItems) {
- if (clusterTask != null && clusterTask.isInterrupted()) {
- return;
- }
- leftItems.add(item);
- }
- } else if (maxClusteringZoom >= mapView.getModel().mapViewPosition
- .getZoomLevel()) {
- // else add to a cluster;
- var pos = mapView.getMapViewProjection().toPixels(item.getLatLong());
- // check existing cluster
- synchronized (clusters) {
- for (Cluster mCluster : clusters) {
- if (clusterTask != null && clusterTask.isInterrupted()) {
- return;
- }
- if (mCluster.getItems().size() == 0) {
- throw new IllegalArgumentException("cluster.getItems().size() == 0");
- }
-
- // find a cluster which contains the marker.
- // use 1st element to fix the location, hinder the cluster from
- // running around and isClustering.
- var gpCenter = mCluster.getItems().get(0).getLatLong();
- if (gpCenter == null) {
- throw new IllegalArgumentException();
- }
-
- var ptCenter = mapView.getMapViewProjection().toPixels(gpCenter);
- // find a cluster which contains the marker.
- if (pos.distance(ptCenter) <= GRIDSIZE) {
- mCluster.addItem(item);
- return;
- }
- }
- // No cluster contain the marker, create a new cluster.
- clusters.add(createCluster(item));
- }
- } else {
- // No clustering allowed, create a new cluster with single item.
- synchronized (clusters) {
- clusters.add(createCluster(item));
- }
- }
- }
-
- /**
- * Create Cluster Object. override this method, if you want to use custom
- * GeoCluster class.
- *
- * @param item GeoItem to be set to cluster.
- */
- public Cluster createCluster(T item) {
- return new Cluster<>(this, item);
- }
-
- /**
- * redraws clusters
- */
- public synchronized void redraw() {
- synchronized (clusters) {
- if (!isClustering) {
- List> removed = new ArrayList<>();
- List> singles = new ArrayList<>();
- clusters.stream()
- .filter(mCluster -> mCluster.getSize() < MIN_CLUSTER_SIZE)
- .forEach(mCluster -> {
- mCluster.getItems().forEach(item -> singles.add(createCluster(item)));
- mCluster.clear();
- removed.add(mCluster);
- });
- clusters.removeAll(removed);
- clusters.addAll(singles);
-
- for (Cluster mCluster : clusters) {
- mCluster.redraw();
- }
- }
- }
- }
-
- /**
- * check if the item is within current viewport.
- *
- * @return true if item is within viewport.
- */
- protected boolean isItemInViewport(GeoItem item) {
- var curBounds = getCurBounds();
- return curBounds != null && curBounds.contains(item.getLatLong());
- }
-
- /**
- * get the current BoundingBox of the viewport
- *
- * @return current BoundingBox of the viewport
- */
- protected synchronized BoundingBox getCurBounds() {
- if (currBoundingBox == null) {
- if (mapView == null) {
- throw new NullPointerException("mapView == null");
- }
- if (mapView.getWidth() <= 0 || mapView.getHeight() <= 0) {
- throw new IllegalArgumentException(" mapView.getWidth() <= 0 " +
- "|| mapView.getHeight() <= 0 "
- + mapView.getWidth() + " || " + mapView.getHeight());
- }
- /* North-West geo point of the bound */
- var nw_ = mapView.getMapViewProjection().fromPixels(-mapView.getWidth() * 0.5, -mapView.getHeight() * 0.5);
- /* South-East geo point of the bound */
- var se_ = mapView.getMapViewProjection().fromPixels(mapView.getWidth() + mapView.getWidth() * 0.5,
- mapView.getHeight() + mapView.getHeight() * 0.5);
- if (se_ != null && nw_ != null) {
- if (se_.latitude > nw_.latitude) {
- currBoundingBox = new BoundingBox(nw_.latitude, se_.longitude, se_.latitude,
- nw_.longitude);
- } else {
- currBoundingBox = new BoundingBox(se_.latitude, nw_.longitude, nw_.latitude,
- se_.longitude);
- }
- }
- }
- return currBoundingBox;
- }
-
- /**
- * add items that were not clustered in last isClustering.
- */
- private void addLeftItems() {
- if (leftItems.size() == 0) {
- return;
- }
- ArrayList currentLeftItems = new ArrayList<>(leftItems);
-
- synchronized (leftItems) {
- leftItems.clear();
- }
- currentLeftItems.forEach(this::addItem);
- }
-
- // *********************************************************************************************************************
- // Methods to implement 'Observer'
- // *********************************************************************************************************************
-
- @Override
- public synchronized void onChange() {
- currBoundingBox = null;
- if (isClustering) {
- return;
- }
-
- if (oldZoolLevel != mapView.getModel().mapViewPosition.getZoomLevel()) {
- // react on zoom changes
- oldZoolLevel = mapView.getModel().mapViewPosition.getZoomLevel();
- resetViewport(false);
- } else {
- // react on position changes
- var mapViewPosition = mapView.getModel().mapViewPosition;
-
- var posOld = mapView.getMapViewProjection().toPixels(oldCenterLatLong);
- var posNew = mapView.getMapViewProjection().toPixels(mapViewPosition.getCenter());
- if (posOld != null && posOld.distance(posNew) > GRIDSIZE / 2) {
- // Log.d(TAG, "moving...");
- oldCenterLatLong = mapViewPosition.getCenter();
- resetViewport(true);
- }
- }
- }
-
- /**
- * reset current viewport, re-cluster the items when zoom has changed, else
- * add not clustered items .
- */
- private synchronized void resetViewport(boolean isMoving) {
- isClustering = true;
- clusterTask = new ClusterTask(isMoving);
- clusterTask.start();
- }
-
- public void cancelClusterTask() {
- if (clusterTask != null) {
- clusterTask.interrupt();
- }
- }
-
- public synchronized void destroyGeoClusterer() {
- synchronized (clusters) {
- clusters.forEach(cluster -> {
- cluster.getClusterManager().cancelClusterTask();
- cluster.clear();
- });
- clusters.clear();
- }
- markerIconBmps.forEach(MarkerBitmap::decrementRefCounters);
- synchronized (leftItems) {
- leftItems.clear();
- }
- MarkerBitmap.clearCaptionBitmap();
- }
-
- @Override
- public void onTap(T item) {
- if (tapHandler != null) {
- tapHandler.onTap(item);
- }
- }
-
- private class ClusterTask extends Thread {
-
- private final boolean isMoving;
-
- public ClusterTask(boolean isMoving) {
- this.isMoving = isMoving;
- }
-
- @Override
- public void run() {
- Log.d(TAG, "Run ClusterTask");
- // If the map is moved without zoom-change: Add unclustered items.
- if (isMoving) {
- addLeftItems();
- }
- // If the cluster zoom level changed then destroy the cluster and
- // collect its markers.
- else {
- synchronized (clusters) {
- for (Cluster mCluster : clusters) {
- synchronized (leftItems) {
- leftItems.addAll(mCluster.getItems());
- }
- mCluster.clear();
- }
- }
- synchronized (clusters) {
- clusters.clear();
- }
- if (!isInterrupted()) {
- synchronized (clusters) {
- if (clusters.size() != 0) {
- throw new IllegalArgumentException();
- }
- }
- addLeftItems();
- }
- }
- isClustering = false;
- redraw();
- Log.d(TAG, "ClusterTask finished");
- }
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/ClusterMarker.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/ClusterMarker.java
deleted file mode 100644
index 0a0bd550..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/ClusterMarker.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * Copyright 2009 Huan Erdao
- * Copyright 2014 Martin Vennekamp
- * Copyright 2015 mapsforge.org
- *
- * This program is free software: you can redistribute it and/or modify it under the
- * terms of the GNU Lesser General Public License as published by the Free Software
- * Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
- * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License along with
- * this program. If not, see .
- */
-package de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge;
-
-import static java.util.stream.Collectors.joining;
-
-import android.util.Log;
-
-import org.mapsforge.core.model.BoundingBox;
-import org.mapsforge.core.model.LatLong;
-import org.mapsforge.core.model.Point;
-import org.mapsforge.core.model.Rectangle;
-import org.mapsforge.core.util.MercatorProjection;
-import org.mapsforge.map.layer.Layer;
-
-/**
- * Layer extended class to display Clustered Marker.
- *
- * @param
- */
-public class ClusterMarker extends Layer {
- private static final String TAG = "ClusterMarker";
-
- /**
- * cluster object
- */
- protected final Cluster cluster;
-
- /**
- * icon marker type
- */
- protected int markerType = 0;
-
- /**
- * @param cluster a cluster to be rendered for this marker
- */
- public ClusterMarker(Cluster cluster) {
- this.cluster = cluster;
- }
-
- /**
- * change icon bitmaps according to the state and content size.
- */
- private void setMarkerBitmap() {
- for (markerType = 0; markerType < cluster.getClusterManager().markerIconBmps.size(); markerType++) {
- // Check if the number of items in this cluster is below or equal the limit of the MarkerBitMap
- if (cluster.getItems().size() <= cluster.getClusterManager()
- .markerIconBmps.get(markerType).getItemMax()) {
- return;
- }
- }
- // set the markerType to maximum value ==> reduce markerType by one.
- markerType--;
- }
-
- @Override
- public synchronized void draw(BoundingBox boundingBox, byte zoomLevel,
- org.mapsforge.core.graphics.Canvas canvas, Point topLeftPoint) {
- if (cluster.getClusterManager() == null ||
- this.getLatLong() == null) {
- return;
- }
- setMarkerBitmap();
- var mapSize = MercatorProjection.getMapSize(zoomLevel, this.displayModel.getTileSize());
- var pixelX = MercatorProjection.longitudeToPixelX(this.getLatLong().longitude, mapSize);
- var pixelY = MercatorProjection.latitudeToPixelY(this.getLatLong().latitude, mapSize);
- double halfBitmapWidth;
- double halfBitmapHeight;
- var markerBitmap = cluster.getClusterManager().markerIconBmps.get(markerType);
- var bitmap = markerBitmap.getBitmap(hasPhoto(), ownPhoto(), stationActive(), isPendingUpload());
- try {
- halfBitmapWidth = bitmap.getWidth() / 2f;
- halfBitmapHeight = bitmap.getHeight() / 2f;
- } catch (NullPointerException e) {
- Log.e(ClusterMarker.TAG, e.getMessage(), e);
- return;
- }
- int left = (int) (pixelX - topLeftPoint.x - halfBitmapWidth + markerBitmap.getIconOffset().x);
- int top = (int) (pixelY - topLeftPoint.y - halfBitmapHeight + markerBitmap.getIconOffset().y);
- int right = (left + bitmap.getWidth());
- int bottom = (top + bitmap.getHeight());
- var bitmapRectangle = new Rectangle(left, top, right, bottom);
- var canvasRectangle = new Rectangle(0, 0, canvas.getWidth(), canvas.getHeight());
- if (!canvasRectangle.intersects(bitmapRectangle)) {
- return;
- }
- // Draw bitmap
- canvas.drawBitmap(bitmap, left, top);
-
- // Draw Text
- if (zoomLevel > 13) {
- if (markerType == 0) {
- // Draw bitmap
- var bubble = MarkerBitmap.getBitmapFromTitle(cluster.getTitle(),
- markerBitmap.getPaint());
- canvas.drawBitmap(bubble,
- (int) (left + halfBitmapWidth - bubble.getWidth() / 2),
- (top - bubble.getHeight()));
- } else {
- int x = (int) (left + bitmap.getWidth() * 1.3);
- int y = (int) (top + bitmap.getHeight() * 1.3
- + markerBitmap.getPaint().getTextHeight(cluster.getTitle()) / 2);
- canvas.drawText(cluster.getTitle(), x, y,
- markerBitmap.getPaint());
- }
- }
-
- }
-
- /**
- * get center location of the marker.
- *
- * @return GeoPoint object of current marker center.
- */
- public LatLong getLatLong() {
- return cluster.getLocation();
- }
-
- /**
- * @return Gets the LatLong Position of the Layer Object
- */
- @Override
- public LatLong getPosition() {
- return getLatLong();
- }
-
- @Override
- public synchronized boolean onTap(LatLong geoPoint, Point viewPosition,
- Point tapPoint) {
- if (cluster.getItems().size() == 1 && contains(viewPosition, tapPoint)) {
- Log.w(ClusterMarker.TAG, "The Marker was touched with onTap: "
- + this.getPosition().toString());
- cluster.getClusterManager().onTap(cluster.getItems().get(0));
- requestRedraw();
- return true;
- } else if (contains(viewPosition, tapPoint)) {
- var builder = new StringBuilder(cluster.getItems().size() + " items:")
- .append(cluster.getItems().stream()
- .map(i -> "\n- " + i.getTitle())
- .limit(6)
- .collect(joining()));
-
- if (cluster.getItems().size() > 6) {
- builder.append("\n...");
- }
-
- if (ClusterManager.toast != null) {
- ClusterManager.toast.setText(builder);
- ClusterManager.toast.show();
- }
- }
- return false;
- }
-
- public synchronized boolean contains(Point viewPosition, Point tapPoint) {
- return getBitmapRectangle(viewPosition).contains(tapPoint);
- }
-
- private Rectangle getBitmapRectangle(Point center) {
- var markerBitmap = cluster.getClusterManager().markerIconBmps.get(markerType);
- var bitmap = markerBitmap.getBitmap(hasPhoto(), ownPhoto(), stationActive(), isPendingUpload());
- return new Rectangle(
- center.x - (float) bitmap.getWidth() + markerBitmap.getIconOffset().x,
- center.y - (float) bitmap.getHeight() + markerBitmap.getIconOffset().y,
- center.x + (float) bitmap.getWidth() + markerBitmap.getIconOffset().x,
- center.y + (float) bitmap.getHeight() + markerBitmap.getIconOffset().y);
- }
-
- public Boolean hasPhoto() {
- return (cluster.getItems().size() == 1 &&
- cluster.getItems().get(0).hasPhoto());
- }
-
- public Boolean ownPhoto() {
- return (cluster.getItems().size() == 1 &&
- cluster.getItems().get(0).ownPhoto());
- }
-
- public Boolean stationActive() {
- return (cluster.getItems().size() == 1 &&
- cluster.getItems().get(0).stationActive());
- }
-
- private boolean isPendingUpload() {
- return (cluster.getItems().size() == 1 &&
- cluster.getItems().get(0).isPendingUpload());
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/DbsTileSource.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/DbsTileSource.java
deleted file mode 100644
index 973654a4..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/DbsTileSource.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge;
-
-import org.mapsforge.map.layer.download.tilesource.OnlineTileSource;
-
-public class DbsTileSource extends OnlineTileSource {
-
- public DbsTileSource(String name, String baseUrl) {
- super(new String[]{"osm-prod.noncd.db.de"}, 8100);
-
- setName(name);
- setBaseUrl(baseUrl);
- setName(name).setAlpha(false)
- .setBaseUrl(baseUrl)
- .setParallelRequestsLimit(8).setProtocol("https").setTileSize(256)
- .setZoomLevelMax((byte) 18).setZoomLevelMin((byte) 0);
- setApiKey("P9roW0ePGY9TCRiXh8y5P1T4traxBrWl");
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/GeoItem.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/GeoItem.java
deleted file mode 100644
index f3c6bae3..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/GeoItem.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2009 Huan Erdao
- * Copyright 2014 Martin Vennekamp
- * Copyright 2015 mapsforge.org
- *
- * This program is free software: you can redistribute it and/or modify it under the
- * terms of the GNU Lesser General Public License as published by the Free Software
- * Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
- * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License along with
- * this program. If not, see .
- */
-
-package de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge;
-
-import org.mapsforge.core.model.LatLong;
-
-/**
- * Utility Class to handle GeoItem for ClusterMarker
- */
-public interface GeoItem {
- /**
- * getLatLong
- *
- * @return item location in LatLong.
- */
- LatLong getLatLong();
-
- /**
- * getTitle
- *
- * @return Title of the item, might be used as Caption text.
- */
- String getTitle();
-
- boolean hasPhoto();
-
- boolean ownPhoto();
-
- /**
- * @return true if the station is active
- */
- boolean stationActive();
-
- boolean isPendingUpload();
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/MapsforgeMapView.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/MapsforgeMapView.java
deleted file mode 100644
index 04c150aa..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/MapsforgeMapView.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-
-import org.mapsforge.map.android.view.MapView;
-
-public class MapsforgeMapView extends MapView {
-
- private final GestureDetector gestureDetector;
- private MapDragListener onDragListener;
-
- public MapsforgeMapView(Context context, AttributeSet attributeSet) {
- super(context, attributeSet);
-
- gestureDetector = new GestureDetector(context, new GestureListener());
- }
-
- @SuppressLint("ClickableViewAccessibility")
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- try {
- gestureDetector.onTouchEvent(ev);
- return super.onTouchEvent(ev);
- } catch (Exception ignored) {
- }
- return false;
- }
-
- private class GestureListener extends GestureDetector.SimpleOnGestureListener {
- @Override
- public boolean onDoubleTap(MotionEvent e) {
- if (onDragListener != null) {
- onDragListener.onDrag();
- }
- return true;
- }
-
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2,
- float distanceX, float distanceY) {
- if (onDragListener != null) {
- onDragListener.onDrag();
- }
- return super.onScroll(e1, e2, distanceX, distanceY);
- }
- }
-
- public void setOnMapDragListener(MapDragListener onDragListener) {
- this.onDragListener = onDragListener;
- }
-
- /**
- * Notifies the parent class when a MapView has been dragged
- */
- public interface MapDragListener {
-
- void onDrag();
-
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/MarkerBitmap.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/MarkerBitmap.java
deleted file mode 100644
index e8313e79..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/MarkerBitmap.java
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * Copyright 2009 Huan Erdao
- * Copyright 2014 Martin Vennekamp
- * Copyright 2015 mapsforge.org
- *
- * This program is free software: you can redistribute it and/or modify it under the
- * terms of the GNU Lesser General Public License as published by the Free Software
- * Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
- * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License along with
- * this program. If not, see .
- */
-
-package de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge;
-
-import android.content.Context;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import androidx.core.content.res.ResourcesCompat;
-
-import org.mapsforge.core.graphics.Bitmap;
-import org.mapsforge.core.graphics.Paint;
-import org.mapsforge.core.model.Point;
-import org.mapsforge.map.model.DisplayModel;
-
-import java.lang.ref.WeakReference;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-
-/**
- * Utility Class to handle MarkerBitmap
- * it handles grid offset to display on the map with offset
- */
-public class MarkerBitmap {
-
- private static final Map captionViews = new HashMap<>();
- private static WeakReference contextRef;
-
- /**
- * bitmap object for stations without photo
- */
- private final Bitmap iconBmpWithoutPhoto;
-
- /**
- * bitmap object for stations with photo
- */
- private final Bitmap iconBmpWithPhoto;
-
- /**
- * bitmap object for stations with photo from user
- */
- private final Bitmap iconBmpOwnPhoto;
-
- /**
- * bitmap object for inactive stations without photo
- */
- private final Bitmap iconBmpWithoutPhotoInactive;
-
- /**
- * bitmap object for inactive stations with photo
- */
- private final Bitmap iconBmpWithPhotoInactive;
-
- /**
- * bitmap object for inactive stations with photo from user
- */
- private final Bitmap iconBmpOwnPhotoInactive;
-
- /**
- * bitmap object for stations with photo from user with pending upload
- */
- private final Bitmap iconBmpPendingUpload;
-
- /**
- * Paint object for drawing icon
- */
- private final Paint paint;
-
- /**
- * offset grid of icon in Point.
- * if you are using symmetric icon image, it should be half size of width&height.
- * adjust this parameter to offset the axis of the image.
- */
- private final Point iconOffset;
-
- /**
- * maximum item size for the marker.
- */
- private final int itemSizeMax;
-
- /**
- * text size for icon
- */
- private final float textSize;
-
- /**
- * NOTE: all src* must be same bitmap size.
- *
- * @param srcWithoutPhoto source Bitmap object for stations without photo
- * @param srcWithPhoto source Bitmap object for stations with photo
- * @param srcOwnPhoto source Bitmap object for stations with photo from user
- * @param srcWithoutPhotoInactive source Bitmap object for inactive stations without photo
- * @param srcWithPhotoInactive source Bitmap object for inactive stations with photo
- * @param srcOwnPhotoInactive source Bitmap object for inactive stations with photo from user
- * @param srcPendingUpload source Bitmap object for stations with photo from user with pending upload
- * @param grid grid point to be offset
- * @param textSize text size for icon
- * @param maxSize icon size threshold
- */
- public MarkerBitmap(Context context, Bitmap srcWithoutPhoto, Bitmap srcWithPhoto, Bitmap srcOwnPhoto,
- Bitmap srcWithoutPhotoInactive, Bitmap srcWithPhotoInactive, Bitmap srcOwnPhotoInactive,
- Bitmap srcPendingUpload,
- Point grid, float textSize, int maxSize, Paint paint) {
- MarkerBitmap.contextRef = new WeakReference<>(context);
- iconBmpWithoutPhoto = srcWithoutPhoto;
- iconBmpWithPhoto = srcWithPhoto;
- iconBmpOwnPhoto = srcOwnPhoto;
- iconBmpWithPhotoInactive = srcWithPhotoInactive;
- iconBmpWithoutPhotoInactive = srcWithoutPhotoInactive;
- iconBmpOwnPhotoInactive = srcOwnPhotoInactive;
- iconBmpPendingUpload = srcPendingUpload;
- iconOffset = grid;
- this.textSize = textSize * DisplayModel.getDeviceScaleFactor();
- itemSizeMax = maxSize;
- this.paint = paint;
- this.paint.setTextSize(getTextSize());
- }
-
- public MarkerBitmap(Context context, Bitmap bitmap, Point grid, float textSize, int maxSize, Paint paint) {
- this(context, bitmap, bitmap, bitmap, bitmap, bitmap, bitmap, bitmap, grid, textSize, maxSize, paint);
- }
-
- public static Bitmap getBitmapFromTitle(String title, Paint paint) {
- var context = contextRef.get();
- if (!captionViews.containsKey(title) && context != null) {
- var bubbleView = new TextView(context);
- bubbleView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
- Utils.setBackground(bubbleView, ResourcesCompat.getDrawable(context.getResources(), R.drawable.caption_background, null));
- bubbleView.setGravity(Gravity.CENTER);
- bubbleView.setMaxEms(20);
- bubbleView.setTextSize(10);
- bubbleView.setPadding(5, -2, 5, -2);
- bubbleView.setTextColor(android.graphics.Color.BLACK);
- bubbleView.setText(title);
- //Measure the view at the exact dimensions (otherwise the text won't center correctly)
- int widthSpec = View.MeasureSpec.makeMeasureSpec(paint.getTextWidth(title), View.MeasureSpec.EXACTLY);
- int heightSpec = View.MeasureSpec.makeMeasureSpec(paint.getTextHeight(title), View.MeasureSpec.EXACTLY);
- bubbleView.measure(widthSpec, heightSpec);
-
- //Layout the view at the width and height
- bubbleView.layout(0, 0, paint.getTextWidth(title), paint.getTextHeight(title));
-
- captionViews.put(title, Utils.viewToBitmap(context, bubbleView));
- Objects.requireNonNull(captionViews.get(title)).incrementRefCount(); // FIXME: is never reduced!
- }
- return captionViews.get(title);
- }
-
- protected static void clearCaptionBitmap() {
- captionViews.values().forEach(Bitmap::decrementRefCount);
- captionViews.clear();
- }
-
- /**
- * @return bitmap object according to the state of the stations
- */
- public Bitmap getBitmap(boolean hasPhoto, boolean ownPhoto, boolean stationActive, boolean inbox) {
- if (inbox) {
- return iconBmpPendingUpload;
- }
-
- if (ownPhoto) {
- if (stationActive) {
- return iconBmpOwnPhoto;
- }
- return iconBmpOwnPhotoInactive;
- } else if (hasPhoto) {
- if (stationActive) {
- return iconBmpWithPhoto;
- }
- return iconBmpWithPhotoInactive;
- }
-
- if (stationActive) {
- return iconBmpWithoutPhoto;
- }
-
- return iconBmpWithoutPhotoInactive;
- }
-
- /**
- * @return get offset of the icon
- */
- public Point getIconOffset() {
- return iconOffset;
- }
-
- /**
- * @return text size already adjusted with DisplayModel.getDeviceScaleFactor(), i.e.
- * the scaling factor for fonts displayed on the display.
- */
- public float getTextSize() {
- return textSize;
- }
-
- /**
- * @return icon size threshold
- */
- public int getItemMax() {
- return itemSizeMax;
- }
-
- /**
- * @return Paint object for drawing icon
- */
- public Paint getPaint() {
- return paint;
- }
-
- public void decrementRefCounters() {
- if (iconBmpOwnPhoto != null) {
- iconBmpOwnPhoto.decrementRefCount();
- }
- if (iconBmpWithPhoto != null) {
- iconBmpWithPhoto.decrementRefCount();
- }
- if (iconBmpWithoutPhoto != null) {
- iconBmpWithoutPhoto.decrementRefCount();
- }
- if (iconBmpOwnPhotoInactive != null) {
- iconBmpOwnPhotoInactive.decrementRefCount();
- }
- if (iconBmpWithPhotoInactive != null) {
- iconBmpWithPhotoInactive.decrementRefCount();
- }
- if (iconBmpWithoutPhotoInactive != null) {
- iconBmpWithoutPhotoInactive.decrementRefCount();
- }
- }
-}
-
-
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/TapHandler.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/TapHandler.java
deleted file mode 100644
index 87d7263f..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/TapHandler.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge;
-
-public interface TapHandler {
-
- void onTap(T item);
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/Utils.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/Utils.java
deleted file mode 100644
index 97eba574..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/mapsforge/Utils.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2013-2014 Ludwig M Brinckmann
- * Copyright 2014, 2015 devemux86
- *
- * This program is free software: you can redistribute it and/or modify it under the
- * terms of the GNU Lesser General Public License as published by the Free Software
- * Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
- * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License along with
- * this program. If not, see .
- */
-package de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge;
-
-import android.content.Context;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.view.View;
-import android.view.View.MeasureSpec;
-
-import org.mapsforge.core.graphics.Bitmap;
-import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
-
-/**
- * Utility functions that can be used across different mapsforge based
- * activities.
- */
-public final class Utils {
-
- /**
- * Compatibility method.
- *
- * @param view the view to set the background on
- * @param background the background
- */
- public static void setBackground(View view, Drawable background) {
- view.setBackground(background);
- }
-
- public static Bitmap viewToBitmap(Context c, View view) {
- view.measure(MeasureSpec.getSize(view.getMeasuredWidth()),
- MeasureSpec.getSize(view.getMeasuredHeight()));
- view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
- view.setDrawingCacheEnabled(true);
- var drawable = new BitmapDrawable(c.getResources(),
- android.graphics.Bitmap.createBitmap(view.getDrawingCache()));
- view.setDrawingCacheEnabled(false);
- return AndroidGraphicFactory.convertToBitmap(drawable);
- }
-
- private Utils() {
- throw new IllegalStateException();
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofNotificationManager.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofNotificationManager.java
deleted file mode 100644
index 8332c7ce..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofNotificationManager.java
+++ /dev/null
@@ -1,251 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.notification;
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.TaskStackBuilder;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-
-import java.util.Set;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.DetailsActivity;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.UploadActivity;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.Timetable;
-
-public abstract class NearbyBahnhofNotificationManager {
- private static final int NOTIFICATION_ID = 1;
- private static final int REQUEST_MAP = 0x10;
- private static final int REQUEST_DETAIL = 0x20;
- private static final int REQUEST_TIMETABLE = 0x30;
- private static final int REQUEST_STATION = 0x40;
- protected final String TAG = NearbyBahnhofNotificationManager.class.getSimpleName();
-
- public static final String CHANNEL_ID = "bahnhoefe_channel_01";// The id of the channel.
-
- private static final String DB_BAHNHOF_LIVE_PKG = "de.deutschebahn.bahnhoflive";
- private static final String DB_BAHNHOF_LIVE_CLASS = "de.deutschebahn.bahnhoflive.MeinBahnhofActivity";
- private final Set countries;
-
- /**
- * The Bahnhof about which a notification is being built.
- */
- protected Station notificationStation;
-
- /**
- * The distance of the Bahnhof about which a notification is being built.
- */
- protected final double notificationDistance;
-
- /**
- * The Android Context for which the notification is generated.
- */
- protected Context context;
-
- /**
- * Constructor. After construction, you need to call notifyUser for action to happen.
- *
- * @param context the Android Context from which this object ist created.
- * @param station the station to issue a notification for.
- * @param distance a double giving the distance from current location to bahnhof (in km)
- */
- public NearbyBahnhofNotificationManager(@NonNull Context context, @NonNull Station station, double distance, Set countries) {
- this.context = context;
- notificationDistance = distance;
- this.notificationStation = station;
- this.countries = countries;
- }
-
- /**
- * Build a notification for a station with Photo. The start command.
- */
- public abstract void notifyUser();
-
- /**
- * Called back once the notification was built up ready.
- */
- protected void onNotificationReady(Notification notification) {
- if (context == null) {
- return; // we're already destroyed
- }
-
- // Get an instance of the NotificationManager service
- var notificationManager = NotificationManagerCompat.from(context);
-
- // Build the notification and issues it with notification manager.
- notificationManager.notify(NOTIFICATION_ID, notification);
- }
-
- /**
- * Helper method that configures a NotificationBuilder wtih the elements common to both
- * notification types.
- */
- protected NotificationCompat.Builder getBasicNotificationBuilder() {
- // Build an intent for an action to see station details
- var detailPendingIntent = getDetailPendingIntent();
- // Build an intent to see the station on a map
- var mapPendingIntent = getMapPendingIntent();
- // Build an intent to view the station's timetable
- var countryByCode = Country.getCountryByCode(countries, notificationStation.getCountry());
- var timetablePendingIntent = countryByCode.map(country -> getTimetablePendingIntent(country, notificationStation)).orElse(null);
-
- createChannel(context);
-
- // Texts and bigStyle
- var textCreator = new TextCreator().invoke();
- var shortText = textCreator.getShortText();
- var bigStyle = textCreator.getBigStyle();
-
- var builder = new NotificationCompat.Builder(context, CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_logotrain_found)
- .setContentTitle(context.getString(R.string.station_is_near))
- .setContentText(shortText)
- .setContentIntent(detailPendingIntent)
- .addAction(R.drawable.ic_directions_white_24dp,
- context.getString(de.bahnhoefe.deutschlands.bahnhofsfotos.R.string.label_map), mapPendingIntent)
- .setStyle(bigStyle)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setOnlyAlertOnce(true)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
-
- if (timetablePendingIntent != null) {
- builder.addAction(R.drawable.ic_timetable,
- context.getString(de.bahnhoefe.deutschlands.bahnhofsfotos.R.string.label_timetable),
- timetablePendingIntent);
- }
-
- builder = new NotificationCompat.Builder(context, CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_logotrain_found)
- .setContentTitle(context.getString(R.string.station_is_near))
- .setContentText(shortText)
- .setContentIntent(detailPendingIntent)
- .addAction(R.drawable.ic_directions_white_24dp,
- context.getString(de.bahnhoefe.deutschlands.bahnhofsfotos.R.string.label_map), mapPendingIntent)
- .addAction(R.drawable.ic_timetable, context.getString(de.bahnhoefe.deutschlands.bahnhofsfotos.R.string.label_timetable), timetablePendingIntent)
- .setStyle(bigStyle)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setOnlyAlertOnce(true)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
-
- return builder;
- }
-
- public static void createChannel(Context context) {
- var notificationManager =
- (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_name), NotificationManager.IMPORTANCE_DEFAULT));
- }
-
- protected PendingIntent pendifyMe(Intent intent, int requestCode) {
- var stackBuilder = TaskStackBuilder.create(context);
- stackBuilder.addNextIntent(intent);
- try {
- stackBuilder.addNextIntentWithParentStack(intent); // syntesize a back stack from the parent information in manifest
- } catch (IllegalArgumentException iae) {
- // unfortunately, this occurs if the supplied intent is not handled by our app
- // in this case, just add the the intent...
- stackBuilder.addNextIntent(intent);
- }
- return stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_CANCEL_CURRENT);
- }
-
- @NonNull
- protected Intent getUploadActivity() {
- // Build an intent for an action to see station details
- var detailIntent = new Intent(context, UploadActivity.class);
- detailIntent.putExtra(UploadActivity.EXTRA_STATION, notificationStation);
- return detailIntent;
- }
-
- @NonNull
- protected PendingIntent getDetailPendingIntent() {
- return pendifyMe(getUploadActivity(), REQUEST_DETAIL);
- }
-
- /**
- * Build an intent for an action to view a map.
- *
- * @return the PendingIntent built.
- */
- protected PendingIntent getMapPendingIntent() {
- var mapIntent = new Intent(Intent.ACTION_VIEW);
- mapIntent.setData(Uri.parse("geo:" + notificationStation.getLat() + "," + notificationStation.getLon()));
- return pendifyMe(mapIntent, REQUEST_MAP);
- }
-
- /**
- * Build an intent for an action to view a timetable for the station.
- *
- * @return the PendingIntent built.
- */
- protected
- @Nullable
- PendingIntent getTimetablePendingIntent(Country country, Station station) {
- var timetableIntent = new Timetable().createTimetableIntent(country, station);
- if (timetableIntent != null) {
- return pendifyMe(timetableIntent, NearbyBahnhofNotificationManager.REQUEST_TIMETABLE);
- }
- return null;
- }
-
- /**
- * Build an intent for an action to view a map.
- *
- * @return the PendingIntent built.
- */
- @NonNull
- protected PendingIntent getStationPendingIntent() {
- // Build an intent for an action to see station details
- var stationIntent = new Intent().setClassName(DB_BAHNHOF_LIVE_PKG, DB_BAHNHOF_LIVE_CLASS);
- stationIntent.putExtra(DetailsActivity.EXTRA_STATION, notificationStation);
- return pendifyMe(stationIntent, REQUEST_STATION);
- }
-
-
- public void destroy() {
- NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID);
- notificationStation = null;
- context = null;
- }
-
- public Station getStation() {
- return notificationStation;
- }
-
- protected class TextCreator {
- private String shortText;
- private NotificationCompat.BigTextStyle bigStyle;
-
- public String getShortText() {
- return shortText;
- }
-
- public NotificationCompat.BigTextStyle getBigStyle() {
- return bigStyle;
- }
-
- public TextCreator invoke() {
- shortText = context.getString(R.string.template_short_text, notificationStation.getTitle(), notificationDistance);
- var longText = context.getString(R.string.template_long_text,
- notificationStation.getTitle(),
- notificationDistance,
- (notificationStation.hasPhoto() ?
- context.getString(R.string.photo_exists) :
- ""));
-
- bigStyle = new NotificationCompat.BigTextStyle();
- bigStyle.bigText(longText);
- return this;
- }
- }
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofNotificationManagerFactory.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofNotificationManagerFactory.java
deleted file mode 100644
index c05a6451..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofNotificationManagerFactory.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.notification;
-
-import android.content.Context;
-
-import java.util.Set;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-
-public class NearbyBahnhofNotificationManagerFactory {
-
- /**
- * Construct the appropriate subclass of NearbyBahnhofNotificationManager for the given parameters.
- *
- * @param context the Android Context to construct for
- * @param station the Bahnhof that is going to be shown to the user
- * @param distance the distance of the station from current position of the user
- * @return an instance of NearbyBahnhofNotificationManager
- */
- static public NearbyBahnhofNotificationManager create(Context context, Station station, double distance, Set countries) {
- if (station.hasPhoto()) {
- return new NearbyBahnhofWithPhotoNotificationManager(context, station, distance, countries);
- } else {
- return new NearbyBahnhofWithoutPhotoNotificationManager(context, station, distance, countries);
- }
- }
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofWithPhotoNotificationManager.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofWithPhotoNotificationManager.java
deleted file mode 100644
index b86bb0f0..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofWithPhotoNotificationManager.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.notification;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.content.res.AppCompatResources;
-import androidx.core.app.NotificationCompat;
-
-import java.util.Set;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.BitmapAvailableHandler;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.BitmapCache;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.ConnectionUtil;
-
-public class NearbyBahnhofWithPhotoNotificationManager extends NearbyBahnhofNotificationManager implements BitmapAvailableHandler {
-
- private static final long[] VIBRATION_PATTERN = new long[]{300};
- private static final int LED_COLOR = 0x00ff0000;
- public static final int BITMAP_HEIGHT = 400;
- public static final int BITMAP_WIDTH = 400;
-
- public NearbyBahnhofWithPhotoNotificationManager(Context context, Station station, double distance, Set countries) {
- super(context, station, distance, countries);
- Log.d(TAG, "Creating " + getClass().getSimpleName());
- }
-
- /**
- * Build a notification for a station with Photo
- */
- @Override
- public void notifyUser() {
- if (ConnectionUtil.checkInternetConnection(context)) {
- BitmapCache.getInstance().getPhoto(this, notificationStation.getPhotoUrl());
- }
- }
-
-
- /**
- * This gets called if the requested bitmap is available. Finish and issue the notification.
- *
- * @param bitmap the fetched Bitmap for the notification. May be null
- */
- @Override
- public void onBitmapAvailable(@Nullable Bitmap bitmap) {
- if (context == null) {
- return; // we're already destroyed
- }
-
- if (bitmap == null) {
- bitmap = getBitmapFromResource(R.drawable.ic_stations_with_photo);
- }
-
- var bigPictureStyle = new NotificationCompat.BigPictureStyle();
- if (bitmap != null) {
- bigPictureStyle.bigPicture(bitmap).setBigContentTitle(null).setSummaryText(notificationStation.getLicense());
- }
-
- var notificationBuilder = getBasicNotificationBuilder()
- .setStyle(bigPictureStyle)
- .extend(new NotificationCompat.WearableExtender())
- .setVibrate(VIBRATION_PATTERN)
- .setColor(LED_COLOR);
-
- onNotificationReady(notificationBuilder.build());
- }
-
- /**
- * Construct a Bitmap object from a given drawable resource ID.
- *
- * @param id the resource ID denoting a drawable resource
- * @return the Bitmap. May be null.
- */
- private Bitmap getBitmapFromResource(int id) {
- var vectorDrawable = AppCompatResources.getDrawable(context, id);
- assert vectorDrawable != null;
- vectorDrawable.setBounds(0, 0, BITMAP_WIDTH, BITMAP_HEIGHT);
- var bm = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888);
- var canvas = new Canvas(bm);
- canvas.drawColor(Color.WHITE);
- vectorDrawable.draw(canvas);
- return bm;
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofWithoutPhotoNotificationManager.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofWithoutPhotoNotificationManager.java
deleted file mode 100644
index 9a95017a..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/notification/NearbyBahnhofWithoutPhotoNotificationManager.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.notification;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.util.Log;
-
-import java.util.Set;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-
-public class NearbyBahnhofWithoutPhotoNotificationManager extends NearbyBahnhofNotificationManager {
-
- private static final long[] VIBRATION_PATTERN = new long[]{300, 100, 300, 100, 300};
- public static final int LED_COLOR = 0x0000ffff;
- private static final int REQUEST_FOTO = 0x100;
-
- public NearbyBahnhofWithoutPhotoNotificationManager(Context context, Station station, double distance, Set countries) {
- super(context, station, distance, countries);
- Log.d(TAG, "Creating " + getClass().getSimpleName());
- }
-
- // helpers that create notification elements that are common to "with foto" and "without foto"
- private PendingIntent getFotoPendingIntent() {
- // Build an intent for an action to take a picture
- // actually this launches UploadActivity with a specific Extra that causes it to launch
- // Photo immediately.
- var intent = getUploadActivity();
- return pendifyMe(intent, REQUEST_FOTO);
- }
-
- /**
- * Build a notification for a station without photo. Call onNotificationReady if done.
- */
- @Override
- public void notifyUser() {
- var notificationBuilder = getBasicNotificationBuilder();
-
- var fotoPendingIntent = getFotoPendingIntent();
-
- notificationBuilder
- .addAction(R.drawable.ic_photo_camera_white_48px, context.getString(R.string.photo), fotoPendingIntent)
- .setVibrate(VIBRATION_PATTERN)
- .setColor(LED_COLOR);
-
- onNotificationReady(notificationBuilder.build());
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/rsapi/RSAPI.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/rsapi/RSAPI.java
deleted file mode 100644
index 7bdb8255..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/rsapi/RSAPI.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi;
-
-import java.util.List;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.ChangePassword;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.HighScore;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.InboxResponse;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.InboxStateQuery;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PhotoStations;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.ProblemReport;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Profile;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PublicInbox;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Statistic;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Token;
-import okhttp3.RequestBody;
-import retrofit2.Call;
-import retrofit2.http.Body;
-import retrofit2.http.DELETE;
-import retrofit2.http.Field;
-import retrofit2.http.FormUrlEncoded;
-import retrofit2.http.GET;
-import retrofit2.http.Header;
-import retrofit2.http.Headers;
-import retrofit2.http.POST;
-import retrofit2.http.Path;
-import retrofit2.http.Query;
-
-public interface RSAPI {
-
- String TAG = RSAPI.class.getSimpleName();
-
- @GET("/countries")
- Call> getCountries();
-
- @GET("/stats")
- Call getStatistic(@Query("country") String country);
-
- @GET("/photographers")
- Call getHighScore(@Query("country") String country);
-
- @GET("/photographers")
- Call getHighScore();
-
- @GET("/myProfile")
- Call getProfile(@Header("Authorization") String authorization);
-
- @Headers({
- "Content-Type: application/json"
- })
- @POST("/myProfile")
- Call saveProfile(@Header("Authorization") String authorization, @Body Profile profile);
-
- @Headers({
- "Content-Type: application/json"
- })
-
- @POST("/changePassword")
- Call changePassword(@Header("Authorization") String authorization, @Body ChangePassword changePassword);
-
- @POST("/photoUpload")
- Call photoUpload(@Header("Authorization") String authorization,
- @Header("Station-Id") String stationId,
- @Header("Country") String countryCode,
- @Header("Station-Title") String stationTitle,
- @Header("Latitude") Double latitude,
- @Header("Longitude") Double longitude,
- @Header("Comment") String comment,
- @Header("Active") Boolean active,
- @Body RequestBody file);
-
- @Headers({
- "Content-Type: application/json"
- })
- @POST("/userInbox")
- Call> queryUploadState(@Header("Authorization") String authorization,
- @Body List inboxStateQueries);
-
- @Headers({
- "Content-Type: application/json"
- })
- @POST("/reportProblem")
- Call reportProblem(@Header("Authorization") String authorization,
- @Body ProblemReport problemReport);
-
- @GET("/publicInbox")
- Call> getPublicInbox();
-
- @POST("/resendEmailVerification")
- Call resendEmailVerification(@Header("Authorization") String authorization);
-
- @GET("/photoStationById/{country}/{id}")
- Call getPhotoStationById(@Path("country") String country, @Path("id") String id);
-
- @GET("/photoStationsByCountry/{country}")
- Call getPhotoStationsByCountry(@Path("country") String country);
-
- @FormUrlEncoded
- @POST("/oauth2/token")
- Call requestAccessToken(@Field("code") String code, @Field("client_id") String clientId, @Field("grant_type") String grantType, @Field("redirect_uri") String redirectUri, @Field("code_verifier") String codeVerifier);
-
- @FormUrlEncoded
- @POST("/oauth2/revoke")
- Call revokeToken(@Field("token") String accessToken, @Field("token_type_hint") String tokenTypeHint);
-
- @DELETE("/myProfile")
- Call deleteAccount(@Header("Authorization") String authorization);
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/rsapi/RSAPIClient.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/rsapi/RSAPIClient.java
deleted file mode 100644
index 46599f52..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/rsapi/RSAPIClient.java
+++ /dev/null
@@ -1,286 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi;
-
-import android.content.Context;
-import android.net.Uri;
-import android.os.Build;
-import android.util.Log;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-
-import com.google.gson.GsonBuilder;
-
-import java.io.IOException;
-import java.security.NoSuchAlgorithmException;
-import java.util.List;
-import java.util.Locale;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.BaseApplication;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.BuildConfig;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.ChangePassword;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.HighScore;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.InboxResponse;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.InboxStateQuery;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.License;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PhotoStations;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.ProblemReport;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Profile;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PublicInbox;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Statistic;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Token;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.util.PKCEUtil;
-import okhttp3.Interceptor;
-import okhttp3.OkHttpClient;
-import okhttp3.RequestBody;
-import okhttp3.logging.HttpLoggingInterceptor;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-import retrofit2.Retrofit;
-import retrofit2.converter.gson.GsonConverterFactory;
-
-public class RSAPIClient {
-
- private static final String TAG = RSAPIClient.class.getSimpleName();
- private final String redirectUri;
- private RSAPI api;
- private String baseUrl;
- private final String clientId;
- private Token token;
- private PKCEUtil pkce;
-
- public RSAPIClient(String baseUrl, String clientId, String accessToken, String redirectUri) {
- this.baseUrl = baseUrl;
- this.clientId = clientId;
- this.redirectUri = redirectUri;
- if (accessToken != null) {
- this.token = new Token(
- accessToken,
- "Bearer");
- }
- api = createRSAPI();
- }
-
- public void setBaseUrl(String baseUrl) {
- this.baseUrl = baseUrl;
- this.api = createRSAPI();
- }
-
- private String getPkceCodeChallenge() throws NoSuchAlgorithmException {
- pkce = new PKCEUtil();
- return pkce.getCodeChallenge();
- }
-
- private String getPkceCodeVerifier() {
- return pkce != null ? pkce.getCodeVerifier() : null;
- }
-
- private RSAPI createRSAPI() {
- var gson = new GsonBuilder();
- gson.registerTypeAdapter(HighScore.class, new HighScore.HighScoreDeserializer());
- gson.registerTypeAdapter(License.class, new License.LicenseDeserializer());
-
- var builder = new OkHttpClient.Builder()
- .addInterceptor(new UserAgentInterceptor())
- .addInterceptor(new AcceptLanguageInterceptor());
-
- if (BuildConfig.DEBUG) {
- var loggingInterceptor = new HttpLoggingInterceptor();
- loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
- builder.addInterceptor(loggingInterceptor);
- }
-
- Retrofit retrofit = new Retrofit.Builder()
- .baseUrl(baseUrl)
- .client(builder.build())
- .addConverterFactory(GsonConverterFactory.create(gson.create()))
- .build();
-
- return retrofit.create(RSAPI.class);
- }
-
- private static class AcceptLanguageInterceptor implements Interceptor {
-
- @NonNull
- @Override
- public okhttp3.Response intercept(@NonNull final Chain chain) throws IOException {
- return chain.proceed(chain.request().newBuilder()
- .header("Accept-Language", Locale.getDefault().toLanguageTag())
- .build());
- }
-
- }
-
- /**
- * This interceptor adds a custom User-Agent.
- */
- public static class UserAgentInterceptor implements Interceptor {
-
- private final String USER_AGENT = BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_NAME + "(" + BuildConfig.VERSION_CODE + "); Android " + Build.VERSION.RELEASE + "/" + Build.VERSION.SDK_INT;
-
- @Override
- @NonNull
- public okhttp3.Response intercept(Interceptor.Chain chain) throws IOException {
- return chain.proceed(chain.request().newBuilder()
- .header("User-Agent", USER_AGENT)
- .build());
- }
- }
-
- public Call> getCountries() {
- return api.getCountries();
- }
-
- public void runUpdateCountriesAndStations(Context context, BaseApplication baseApplication, ResultListener listener) {
- getCountries().enqueue(new Callback<>() {
- @Override
- public void onResponse(@NonNull Call> call, @NonNull Response> response) {
- var body = response.body();
- if (response.isSuccessful() && body != null) {
- baseApplication.getDbAdapter().insertCountries(body);
- }
- }
-
- @Override
- public void onFailure(@NonNull Call> call, @NonNull Throwable t) {
- Log.e(TAG, "Error refreshing countries", t);
- Toast.makeText(context, context.getString(R.string.error_updating_countries) + t.getMessage(), Toast.LENGTH_LONG).show();
- }
- });
-
- var countryCodes = baseApplication.getCountryCodes();
- var overallSuccess = new AtomicBoolean(true);
- var runningRequestCount = new AtomicInteger(countryCodes.size());
- countryCodes.forEach(countryCode -> api.getPhotoStationsByCountry(countryCode).enqueue(new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- var stationList = response.body();
- if (response.isSuccessful() && stationList != null) {
- baseApplication.getDbAdapter().insertStations(stationList, countryCode);
- baseApplication.setLastUpdate(System.currentTimeMillis());
- }
- if (!response.isSuccessful()) {
- overallSuccess.set(false);
- }
- onResult(response.isSuccessful());
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- Log.e(TAG, "Error refreshing stations", t);
- Toast.makeText(context, context.getString(R.string.station_update_failed) + t.getMessage(), Toast.LENGTH_LONG).show();
- onResult(false);
- }
-
- void onResult(boolean success) {
- var stillRunningRequests = runningRequestCount.decrementAndGet();
- if (!success) {
- overallSuccess.set(false);
- }
- if (stillRunningRequests == 0) {
- listener.onResult(overallSuccess.get());
- }
- }
- }));
-
- }
-
- public Call getPhotoStationById(String country, String id) {
- return api.getPhotoStationById(country, id);
- }
-
- public Call getPhotoStationsByCountry(String country) {
- return api.getPhotoStationsByCountry(country);
- }
-
- public Call> getPublicInbox() {
- return api.getPublicInbox();
- }
-
- public Call getHighScore() {
- return api.getHighScore();
- }
-
- public Call getHighScore(String country) {
- return api.getHighScore(country);
- }
-
- public Call reportProblem(ProblemReport problemReport) {
- return api.reportProblem(getUserAuthorization(), problemReport);
- }
-
- private String getUserAuthorization() {
- if (hasToken()) {
- return token.getTokenType() + " " + token.getAccessToken();
- }
- return null;
- }
-
- public Call photoUpload(String stationId, String countryCode,
- String stationTitle, Double latitude,
- Double longitude, String comment,
- Boolean active, RequestBody file) {
- return api.photoUpload(getUserAuthorization(), stationId, countryCode, stationTitle, latitude, longitude, comment, active, file);
- }
-
- public Call> queryUploadState(List stateQueries) {
- return api.queryUploadState(getUserAuthorization(), stateQueries);
- }
-
- public Call getProfile() {
- return api.getProfile(getUserAuthorization());
- }
-
- public Call saveProfile(Profile profile) {
- return api.saveProfile(getUserAuthorization(), profile);
- }
-
- public Call changePassword(String newPassword) {
- return api.changePassword(getUserAuthorization(), new ChangePassword(newPassword));
- }
-
- public Call deleteAccount() {
- return api.deleteAccount(getUserAuthorization());
- }
-
- public Call resendEmailVerification() {
- return api.resendEmailVerification(getUserAuthorization());
- }
-
- public Call getStatistic(String country) {
- return api.getStatistic(country);
- }
-
- public Call requestAccessToken(String code) {
- return api.requestAccessToken(code, clientId, "authorization_code", redirectUri, getPkceCodeVerifier());
- }
-
- public void setToken(Token token) {
- this.token = token;
- }
-
- public boolean hasToken() {
- return token != null;
- }
-
- public void clearToken() {
- this.token = null;
- }
-
- public Uri createAuthorizeUri() throws NoSuchAlgorithmException {
- return Uri.parse(baseUrl + "oauth2/authorize" + "?client_id=" + clientId + "&code_challenge=" + getPkceCodeChallenge() + "&code_challenge_method=S256&scope=all&response_type=code&redirect_uri=" + redirectUri);
- }
-
- public String getRedirectUri() {
- return redirectUri;
- }
-
- public interface ResultListener {
- void onResult(boolean success);
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/BitmapAvailableHandler.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/BitmapAvailableHandler.java
deleted file mode 100644
index 10f6c207..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/BitmapAvailableHandler.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import android.graphics.Bitmap;
-
-import androidx.annotation.Nullable;
-
-/**
- * Callback for BitmapDownloader
- */
-public interface BitmapAvailableHandler {
-
- /**
- * This gets called if the requested bitmap is available. Finish and issue the notification.
- *
- * @param bitmap the fetched Bitmap for the notification. May be null
- */
- void onBitmapAvailable(@Nullable Bitmap bitmap);
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/BitmapCache.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/BitmapCache.java
deleted file mode 100644
index b8957f44..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/BitmapCache.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import android.graphics.Bitmap;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-
-/**
- * A cache for station photos.
- */
-public class BitmapCache {
- private final String TAG = BitmapCache.class.getSimpleName();
-
- /**
- * The singleton.
- */
- private static BitmapCache instance = null;
-
- /**
- * Get the BitmapCache instance.
- *
- * @return the single instance of BitmapCache.
- */
- public static BitmapCache getInstance() {
- synchronized (BitmapCache.class) {
- if (instance == null) {
- instance = new BitmapCache();
- }
- return instance;
- }
- }
-
- private BitmapCache() {
- cache = new HashMap<>(10);
- requests = new HashMap<>(3);
- }
-
- private final HashMap> requests;
-
- private final HashMap cache;
-
- /**
- * Get a picture for the given URL, either from cache or by downloading.
- * The fetching happens asynchronously. When finished, the provided callback interface is called.
- *
- * @param callback the BitmapAvailableHandler to call on completion.
- * @param resourceUrl the URL to fetch
- */
- public void getPhoto(BitmapAvailableHandler callback, @NonNull String resourceUrl) {
- URL url = null;
- try {
- url = new URL(resourceUrl);
- getPhoto(callback, url);
- } catch (MalformedURLException e) {
- Log.e(TAG, "Couldn't load photo from malformed URL " + resourceUrl);
- callback.onBitmapAvailable(null);
- }
- }
-
- /**
- * Get a picture for the given URL, either from cache or by downloading.
- * The fetching happens asynchronously. When finished, the provided callback interface is called.
- *
- * @param callback the BitmapAvailableHandler to call on completion.
- * @param resourceUrl the URL to fetch
- */
- public void getPhoto(BitmapAvailableHandler callback, @NonNull URL resourceUrl) {
- var bitmap = cache.get(resourceUrl);
- if (bitmap == null) {
- var downloader = new BitmapDownloader((bitmap1) -> {
- if (bitmap1 != null) {
- cache.put(resourceUrl, bitmap1);
- }
-
- // inform all requestors about the available image
- synchronized (requests) {
- var handlers = requests.remove(resourceUrl);
- if (handlers == null) {
- Log.wtf(TAG, "Request result without a saved requestor. This should never happen.");
- } else {
- handlers.forEach(handler -> handler.onBitmapAvailable(bitmap1));
- }
- }
- }, resourceUrl);
- synchronized (requests) {
- var handlers = requests.get(resourceUrl);
- if (handlers == null) {
- handlers = new ArrayList<>();
- handlers.add(callback);
- requests.put(resourceUrl, handlers);
- } else {
- handlers.add(callback);
- }
- }
- downloader.start();
- } else {
- callback.onBitmapAvailable(bitmap);
- }
-
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/BitmapDownloader.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/BitmapDownloader.java
deleted file mode 100644
index 4ade5dd9..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/BitmapDownloader.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.util.Log;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-/**
- * Helper class to download image in background
- */
-public class BitmapDownloader extends Thread {
-
- private final String TAG = BitmapDownloader.class.getSimpleName();
- private final BitmapAvailableHandler bitmapAvailableHandler;
- private final URL url;
-
- /**
- * Construct a bitmap Downloader for the given URL
- *
- * @param handler the BitmapAvailableHandler instance that is called on completion
- * @param url the URL to fetch the Bitmap from
- */
- public BitmapDownloader(BitmapAvailableHandler handler, URL url) {
- super();
- this.bitmapAvailableHandler = handler;
- this.url = url;
- }
-
- @Override
- public void run() {
- Bitmap bitmap = null;
- try {
- Log.i(TAG, "Fetching Bitmap from URL: " + url);
- var httpConnection = (HttpURLConnection) url.openConnection();
- try (var is = httpConnection.getInputStream()) {
- if (httpConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
- String contentType = httpConnection.getContentType();
- if (contentType != null && !contentType.startsWith("image")) {
- Log.w(TAG, "Supplied URL does not appear to be an image resource (type=" + contentType + ")");
- }
- bitmap = BitmapFactory.decodeStream(is);
- } else {
- Log.e(TAG, "Error downloading photo: " + httpConnection.getResponseCode());
- }
- }
- } catch (IOException e) {
- Log.e(TAG, "Could not download photo");
- bitmap = null;
- }
- bitmapAvailableHandler.onBitmapAvailable(bitmap);
- }
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/ConnectionUtil.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/ConnectionUtil.java
deleted file mode 100644
index a8e2b620..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/ConnectionUtil.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.widget.Toast;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-
-public class ConnectionUtil {
-
- public static boolean checkInternetConnection(Context context) {
- var cm =
- (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
-
- var activeNetwork = cm.getActiveNetworkInfo();
- var isConnected = activeNetwork != null &&
- activeNetwork.isConnectedOrConnecting();
-
- if (!isConnected) {
- Toast.makeText(context, R.string.no_internet_connection, Toast.LENGTH_LONG).show();
- }
-
- return isConnected;
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/Constants.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/Constants.java
deleted file mode 100644
index 0e716bf8..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/Constants.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-public class Constants {
-
- public static final int STORED_PHOTO_WIDTH = 1920;
- public static final int STORED_PHOTO_QUALITY = 95;
- public static final String CURSOR_ADAPTER_ID = "_id";
-
- /**
- * Columns of Stations table
- */
- public final static class STATIONS {
- public static final String ROWID = "rowid";
- public static final String ID = "id";
- public static final String TITLE = "title";
- public static final String NORMALIZED_TITLE = "normalizedTitle";
- public static final String COUNTRY = "country";
- public static final String LAT = "lat";
- public static final String LON = "lon";
- public static final String PHOTO_ID = "photoId";
- public static final String PHOTO_URL = "photoUrl";
- public static final String PHOTOGRAPHER = "photographer";
- public static final String PHOTOGRAPHER_URL = "photographerUrl";
- public static final String LICENSE = "license";
- public static final String LICENSE_URL = "licenseUrl";
- public static final String DS100 = "DS100";
- public static final String ACTIVE = "active";
- public static final String OUTDATED = "outdated";
- }
-
- /**
- * Columns of Countries table
- */
- public final static class COUNTRIES {
- public static final String COUNTRYNAME = "country";
- public static final String COUNTRYSHORTCODE = "countryflag";
- public static final String EMAIL = "mail";
- public static final String ROWID_COUNTRIES = "rowidcountries";
- public static final String TIMETABLE_URL_TEMPLATE = "timetable_url_template";
- public static final String OVERRIDE_LICENSE = "override_license";
- }
-
- /**
- * Columns of ProviderApps table
- */
- public final static class PROVIDER_APPS {
- public static final String COUNTRYSHORTCODE = "countryflag";
- public static final String PA_TYPE = "type";
- public static final String PA_NAME = "name";
- public static final String PA_URL = "url";
- }
-
- /**
- * Columns of Uploads table
- */
- public final static class UPLOADS {
- public static final String ID = "id";
- public static final String REMOTE_ID = "remoteId";
- public static final String TITLE = "title";
- public static final String COUNTRY = "country";
- public static final String STATION_ID = "stationId";
- public static final String LAT = "lat";
- public static final String LON = "lon";
- public static final String PROBLEM_TYPE = "problemType";
- public static final String INBOX_URL = "inboxUrl";
- public static final String UPLOAD_STATE = "uploadState";
- public static final String REJECTED_REASON = "rejectedReason";
- public static final String CREATED_AT = "createdAt";
- public static final String COMMENT = "comment";
- public static final String JOIN_STATION_TITLE = "stationTitle"; // only for join with station
- public static final String ACTIVE = "active";
- public static final String CRC32 = "crc32";
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/ExceptionHandler.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/ExceptionHandler.java
deleted file mode 100644
index 08809cf3..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/ExceptionHandler.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.BuildConfig;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.ShowErrorActivity;
-
-public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
-
- private final Context context;
- private final Thread.UncaughtExceptionHandler defaultExceptionHandler;
-
- public ExceptionHandler(Context context, Thread.UncaughtExceptionHandler defaultExceptionHandler) {
- this.context = context;
- this.defaultExceptionHandler = defaultExceptionHandler;
- }
-
- @Override
- public void uncaughtException(@NonNull Thread thread, @NonNull Throwable exception) {
- try {
- var errorReport = generateErrorReport(formatException(thread, exception));
- var intent = new Intent(context, ShowErrorActivity.class);
- intent.putExtra(ShowErrorActivity.EXTRA_ERROR_TEXT, errorReport);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
- // Pass exception to OS for graceful handling - OS will report it via ADB
- // and close all activities and services.
- defaultExceptionHandler.uncaughtException(thread, exception);
- } catch (Exception fatalException) {
- // do not recurse into custom handler if exception is thrown during
- // exception handling. Pass this ultimate fatal exception to OS
- defaultExceptionHandler.uncaughtException(thread, fatalException);
- }
- }
-
- private String formatException(Thread thread, Throwable exception) {
- var stringBuilder = new StringBuilder();
- stringBuilder.append(String.format("Exception in thread \"%s\": ", thread.getName()));
-
- // print available stacktrace
- var writer = new StringWriter();
- exception.printStackTrace(new PrintWriter(writer));
- stringBuilder.append(writer);
-
- return stringBuilder.toString();
- }
-
- private String generateErrorReport(String stackTrace) {
- return "### App information\n" +
- "* ID: " + BuildConfig.APPLICATION_ID + "\n" +
- "* Version: " + BuildConfig.VERSION_CODE + " " + BuildConfig.VERSION_NAME + "\n" +
- "\n" +
- "### Device information\n" +
- "* Brand: " + Build.BRAND + "\n" +
- "* Device: " + Build.DEVICE + "\n" +
- "* Model: " + Build.MODEL + "\n" +
- "* Id: " + Build.ID + "\n" +
- "* Product: " + Build.PRODUCT + "\n" +
- "\n" +
- "### Firmware\n" +
- "* SDK: " + Build.VERSION.SDK_INT + "\n" +
- "* Release: " + Build.VERSION.RELEASE + "\n" +
- "* Incremental: " + Build.VERSION.INCREMENTAL + "\n" +
- "\n" +
- "### Cause of error\n" +
- "```java\n" +
- stackTrace + "\n" +
- "```\n";
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/FileUtils.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/FileUtils.java
deleted file mode 100644
index f5e49103..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/FileUtils.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import android.content.Context;
-import android.os.Environment;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.util.Locale;
-import java.util.Objects;
-
-public class FileUtils {
-
- private static final String TAG = FileUtils.class.getSimpleName();
-
- /**
- * Get the base directory for storing fotos
- *
- * @return the File denoting the base directory or null, if cannot write to it
- */
- @Nullable
- public static File getLocalFotoDir(Context context) {
- return mkdirs(Objects.requireNonNull(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)));
- }
-
- private static File mkdirs(File dir) {
- try {
- Files.createDirectories(dir.toPath());
- return dir;
- } catch (IOException e) {
- Log.e(TAG, "Cannot create directory structure " + dir.getAbsolutePath(), e);
- }
- return null;
- }
-
- /**
- * Get the file path for storing this stations foto
- *
- * @return the File
- */
- @Nullable
- public static File getStoredMediaFile(Context context, Long uploadId) {
- var mediaStorageDir = FileUtils.getLocalFotoDir(context);
- if (mediaStorageDir == null) {
- return null;
- }
-
- var storeMediaFile = new File(mediaStorageDir, String.format(Locale.ENGLISH, "%d.jpg", uploadId));
- Log.d(TAG, "StoredMediaFile: " + storeMediaFile);
-
- return storeMediaFile;
- }
-
- public static File getImageCacheFile(Context applicationContext, String imageId) {
- File imagePath = new File(applicationContext.getCacheDir(), "images");
- mkdirs(imagePath);
- return new File(imagePath, imageId + ".jpg");
- }
-
- public static void deleteQuietly(File file) {
- try {
- Files.delete(file.toPath());
- } catch (IOException exception) {
- Log.w(TAG, "unable to delete file " + file, exception);
- }
- }
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/KeyValueSpinnerItem.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/KeyValueSpinnerItem.java
deleted file mode 100644
index c9803fdc..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/KeyValueSpinnerItem.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import androidx.annotation.NonNull;
-
-public class KeyValueSpinnerItem {
-
- private final String spinnerText;
- private final String value;
-
- public KeyValueSpinnerItem(String spinnerText, String value) {
- this.spinnerText = spinnerText;
- this.value = value;
- }
-
- public String getSpinnerText() {
- return spinnerText;
- }
-
- public String getValue() {
- return value;
- }
-
- @Override
- @NonNull
- public String toString() {
- return spinnerText;
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/NavItem.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/NavItem.java
deleted file mode 100644
index 4c08e901..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/NavItem.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.MapsActivity;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-
-public enum NavItem {
-
- OEPNV(R.string.nav_oepnv, R.drawable.ic_directions_bus_gray_24px, "google.navigation:ll=%s,%s&mode=Transit"),
- CAR(R.string.nav_car, R.drawable.ic_directions_car_gray_24px, "google.navigation:ll=%s,%s&mode=d"),
- BIKE(R.string.nav_bike, R.drawable.ic_directions_bike_gray_24px, "google.navigation:ll=%s,%s&mode=b"),
- WALK(R.string.nav_walk, R.drawable.ic_directions_walk_gray_24px, "google.navigation:ll=%s,%s&mode=w"),
- SHOW(R.string.nav_show, R.drawable.ic_info_gray_24px, "geo:0,0?q=%s,%s(%s)"),
- SHOW_ON_MAP(R.string.nav_show_on_map, R.drawable.ic_map_gray_24px, null) {
- @Override
- public Intent createIntent(Context packageContext, double lat, double lon, String text, int markerRes) {
- var intent = new Intent(packageContext, MapsActivity.class);
- intent.putExtra(MapsActivity.EXTRAS_LATITUDE, lat);
- intent.putExtra(MapsActivity.EXTRAS_LONGITUDE, lon);
- intent.putExtra(MapsActivity.EXTRAS_MARKER, markerRes);
- return intent;
- }
- };
-
- private final int textRes;
- private final int iconRes;
- private final String uriTemplate;
-
- NavItem(int textRes, int iconRes, String uriTemplate) {
- this.textRes = textRes;
- this.iconRes = iconRes;
- this.uriTemplate = uriTemplate;
- }
-
- public int getTextRes() {
- return textRes;
- }
-
- public int getIconRes() {
- return iconRes;
- }
-
- public Intent createIntent(Context packageContext, double lat, double lon, String text, int markerRes) {
- var uriString = String.format(uriTemplate, lat, lon, text);
- return new Intent(Intent.ACTION_VIEW, Uri.parse(uriString));
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/PKCEUtil.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/PKCEUtil.java
deleted file mode 100644
index 5b6d4d06..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/PKCEUtil.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import java.nio.charset.Charset;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Base64;
-
-public class PKCEUtil {
-
- private String verifier = "";
-
- public String getCodeVerifier() {
- return verifier;
- }
-
- public String getCodeChallenge() throws NoSuchAlgorithmException {
- verifier = generateCodeVerifier();
- return generateCodeChallenge(verifier);
- }
-
- private String generateCodeVerifier() {
- var secureRandom = new SecureRandom();
- var codeVerifier = new byte[32];
- secureRandom.nextBytes(codeVerifier);
- return Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifier);
- }
-
- private String generateCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
- var bytes = codeVerifier.getBytes(Charset.defaultCharset());
- var messageDigest = MessageDigest.getInstance("SHA-256");
- messageDigest.update(bytes, 0, bytes.length);
- var digest = messageDigest.digest();
- return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/StationFilter.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/StationFilter.java
deleted file mode 100644
index 808d52ad..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/StationFilter.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import android.content.Context;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.R;
-
-public class StationFilter {
-
- private Boolean photo;
-
- private Boolean active;
-
- private String nickname;
-
- public StationFilter(Boolean photo, Boolean active, String nickname) {
- this.photo = photo;
- this.active = active;
- this.nickname = nickname;
- }
-
- public Boolean hasPhoto() {
- return photo;
- }
-
- public void setPhoto(Boolean photo) {
- this.photo = photo;
- }
-
- public Boolean isActive() {
- return active;
- }
-
- public void setActive(Boolean active) {
- this.active = active;
- }
-
- public String getNickname() {
- return nickname;
- }
-
- public void setNickname(String nickname) {
- this.nickname = nickname;
- }
-
- public int getPhotoIcon() {
- if (photo == null) {
- return R.drawable.ic_photo_inactive_24px;
- } else if (photo) {
- return R.drawable.ic_photo_active_24px;
- }
- return R.drawable.ic_photo_missing_active_24px;
- }
-
- public int getNicknameIcon() {
- return nickname == null ? R.drawable.ic_person_inactive_24px : R.drawable.ic_person_active_24px;
- }
-
- public int getActiveIcon() {
- if (active == null) {
- return R.drawable.ic_station_active_inactive_24px;
- } else if (active) {
- return R.drawable.ic_station_active_active_24px;
- }
- return R.drawable.ic_station_inactive_active_24px;
- }
-
- public int getActiveText() {
- return active == null ? R.string.no_text : active ? R.string.filter_active : R.string.filter_inactive;
- }
-
- public boolean isPhotoFilterActive() {
- return photo != null;
- }
-
- public boolean isActiveFilterActive() {
- return active != null;
- }
-
- public boolean isNicknameFilterActive() {
- return nickname != null;
- }
-
- public String getNicknameText(Context context) {
- return isNicknameFilterActive() ? nickname : context.getString(R.string.no_text);
- }
-
-}
diff --git a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/Timetable.java b/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/Timetable.java
deleted file mode 100644
index 69859cc1..00000000
--- a/app/src/main/java/de/bahnhoefe/deutschlands/bahnhofsfotos/util/Timetable.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package de.bahnhoefe.deutschlands.bahnhofsfotos.util;
-
-import android.content.Intent;
-import android.net.Uri;
-
-import androidx.annotation.Nullable;
-
-import org.apache.commons.lang3.StringUtils;
-
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country;
-import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station;
-
-public class Timetable {
-
- /**
- * Build an intent for an action to view a timetable for the station.
- *
- * @return the PendingIntent built.
- */
- @Nullable
- public Intent createTimetableIntent(Country country, Station station) {
- if (!country.hasTimetableUrlTemplate()) {
- return null;
- }
-
- var timeTableTemplate = country.getTimetableUrlTemplate();
- timeTableTemplate = timeTableTemplate.replace("{id}", station.getId());
- timeTableTemplate = timeTableTemplate.replace("{title}", station.getTitle());
- timeTableTemplate = timeTableTemplate.replace("{DS100}", StringUtils.trimToEmpty(station.getDs100()));
-
- var timetableIntent = new Intent(Intent.ACTION_VIEW);
- timetableIntent.setData(Uri.parse(timeTableTemplate));
- return timetableIntent;
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/BaseApplication.kt b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/BaseApplication.kt
new file mode 100644
index 00000000..6821bce5
--- /dev/null
+++ b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/BaseApplication.kt
@@ -0,0 +1,399 @@
+package de.bahnhoefe.deutschlands.bahnhofsfotos
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.content.Context
+import android.content.SharedPreferences
+import android.location.Location
+import android.net.Uri
+import android.os.Build
+import android.util.Log
+import androidx.multidex.MultiDex
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKey
+import de.bahnhoefe.deutschlands.bahnhofsfotos.db.DbAdapter
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.License
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Profile
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.UpdatePolicy
+import de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi.RSAPIClient
+import de.bahnhoefe.deutschlands.bahnhofsfotos.util.ExceptionHandler
+import de.bahnhoefe.deutschlands.bahnhofsfotos.util.StationFilter
+import org.apache.commons.lang3.StringUtils
+import org.mapsforge.core.model.LatLong
+import org.mapsforge.core.model.MapPosition
+
+class BaseApplication : Application() {
+ lateinit var dbAdapter: DbAdapter
+ lateinit var rsapiClient: RSAPIClient
+ private lateinit var preferences: SharedPreferences
+ private lateinit var encryptedPreferences: SharedPreferences
+
+ init {
+ instance = this
+ }
+
+ override fun attachBaseContext(base: Context) {
+ super.attachBaseContext(base)
+ MultiDex.install(this)
+
+ // handle crashes only outside the crash reporter activity/process
+ if (!isCrashReportingProcess) {
+ Thread.setDefaultUncaughtExceptionHandler(
+ Thread.getDefaultUncaughtExceptionHandler()?.let { ExceptionHandler(this, it) }
+ )
+ }
+ }
+
+ private val isCrashReportingProcess: Boolean
+ get() {
+ var processName: String? = ""
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ // Using the same technique as Application.getProcessName() for older devices
+ // Using reflection since ActivityThread is an internal API
+ try {
+ @SuppressLint("PrivateApi") val activityThread =
+ Class.forName("android.app.ActivityThread")
+ @SuppressLint("DiscouragedPrivateApi") val getProcessName =
+ activityThread.getDeclaredMethod("currentProcessName")
+ processName = getProcessName.invoke(null) as String
+ } catch (ignored: Exception) {
+ }
+ } else {
+ processName = getProcessName()
+ }
+ return processName != null && processName.endsWith(":crash")
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ dbAdapter = DbAdapter(this)
+ dbAdapter.open()
+ preferences = getSharedPreferences(PREF_FILE, MODE_PRIVATE)
+
+ // Creates the instance for the encrypted preferences.
+ encryptedPreferences = try {
+ val masterKey = MasterKey.Builder(this)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build()
+ EncryptedSharedPreferences.create(
+ this,
+ "secret_shared_prefs",
+ masterKey,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ )
+ } catch (e: Exception) {
+ Log.w(
+ TAG,
+ "Unable to create EncryptedSharedPreferences, fallback to unencrypted preferences",
+ e
+ )
+ preferences
+ }
+
+ // migrate access token from unencrypted to encrypted preferences
+ if (encryptedPreferences != preferences
+ && !encryptedPreferences.contains(getString(R.string.ACCESS_TOKEN))
+ && preferences.contains(getString(R.string.ACCESS_TOKEN))
+ ) {
+ accessToken = preferences.getString(getString(R.string.ACCESS_TOKEN), null)
+ preferences.edit().remove(getString(R.string.ACCESS_TOKEN)).apply()
+ }
+
+ // migrate photo owner preference to boolean
+ val photoOwner = preferences.all[getString(R.string.PHOTO_OWNER)]
+ if ("YES" == photoOwner) {
+ this.photoOwner = true
+ }
+ rsapiClient = RSAPIClient(
+ apiUrl!!, getString(R.string.rsapiClientId), accessToken, getString(
+ R.string.rsapiRedirectScheme
+ ) + "://" + getString(R.string.rsapiRedirectHost)
+ )
+ }
+
+ var apiUrl: String?
+ get() {
+ val apiUri = preferences.getString(getString(R.string.API_URL), null)
+ return getValidatedApiUrlString(apiUri)
+ }
+ set(apiUrl) {
+ val validatedUrl = getValidatedApiUrlString(apiUrl)
+ putString(R.string.API_URL, validatedUrl)
+ rsapiClient.setBaseUrl(validatedUrl)
+ }
+
+ private fun putBoolean(key: Int, value: Boolean) {
+ val editor = preferences.edit()
+ editor.putBoolean(getString(key), value)
+ editor.apply()
+ }
+
+ private fun putString(key: Int, value: String?) {
+ val editor = preferences.edit()
+ editor.putString(getString(key), StringUtils.trimToNull(value))
+ editor.apply()
+ }
+
+ private fun putStringSet(key: Int, value: Set?) {
+ val editor = preferences.edit()
+ editor.putStringSet(getString(key), value)
+ editor.apply()
+ }
+
+ private fun putLong(key: Int, value: Long) {
+ val editor = preferences.edit()
+ editor.putLong(getString(key), value)
+ editor.apply()
+ }
+
+ private fun putDouble(key: Int, value: Double) {
+ val editor = preferences.edit()
+ editor.putLong(getString(key), java.lang.Double.doubleToRawLongBits(value))
+ editor.apply()
+ }
+
+ private fun getDouble(key: Int): Double {
+ return if (!preferences.contains(getString(key))) {
+ 0.0
+ } else java.lang.Double.longBitsToDouble(preferences.getLong(getString(key), 0))
+ }
+
+ var countryCodes: Set
+ get() {
+ val oldCountryCode =
+ preferences.getString(getString(R.string.COUNTRY), DEFAULT_COUNTRY)
+ var stringSet = preferences.getStringSet(
+ getString(R.string.COUNTRIES),
+ HashSet(setOf(oldCountryCode))
+ )
+ if (stringSet!!.isEmpty()) {
+ stringSet = HashSet(setOf(DEFAULT_COUNTRY))
+ }
+ return stringSet
+ }
+ set(countryCodes) {
+ putStringSet(R.string.COUNTRIES, countryCodes)
+ }
+ var firstAppStart: Boolean
+ get() = preferences.getBoolean(getString(R.string.FIRSTAPPSTART), DEFAULT_FIRSTAPPSTART)
+ set(firstAppStart) {
+ putBoolean(R.string.FIRSTAPPSTART, firstAppStart)
+ }
+ var license: License?
+ get() = License.byName(
+ preferences.getString(
+ getString(R.string.LICENCE),
+ License.UNKNOWN.toString()
+ )!!
+ )
+ set(license) {
+ putString(R.string.LICENCE, license?.toString() ?: License.UNKNOWN.toString())
+ }
+ var updatePolicy: UpdatePolicy
+ get() = UpdatePolicy.byName(
+ preferences.getString(
+ getString(R.string.UPDATE_POLICY),
+ License.UNKNOWN.toString()
+ )!!
+ )
+ set(updatePolicy) {
+ putString(R.string.UPDATE_POLICY, updatePolicy.toString())
+ }
+ private var photoOwner: Boolean
+ get() = preferences.getBoolean(getString(R.string.PHOTO_OWNER), false)
+ set(photoOwner) {
+ putBoolean(R.string.PHOTO_OWNER, photoOwner)
+ }
+ private var photographerLink: String?
+ get() = preferences.getString(getString(R.string.LINK_TO_PHOTOGRAPHER), DEFAULT)
+ set(photographerLink) {
+ putString(R.string.LINK_TO_PHOTOGRAPHER, photographerLink)
+ }
+ var nickname: String?
+ get() = preferences.getString(getString(R.string.NICKNAME), DEFAULT)
+ set(nickname) {
+ putString(R.string.NICKNAME, nickname)
+ }
+ var email: String?
+ get() = preferences.getString(getString(R.string.EMAIL), DEFAULT)
+ set(email) {
+ putString(R.string.EMAIL, email)
+ }
+ private var isEmailVerified: Boolean
+ get() = preferences.getBoolean(getString(R.string.PHOTO_OWNER), false)
+ set(emailVerified) {
+ putBoolean(R.string.PHOTO_OWNER, emailVerified)
+ }
+ var accessToken: String?
+ get() = encryptedPreferences.getString(getString(R.string.ACCESS_TOKEN), null)
+ set(apiToken) {
+ val editor = encryptedPreferences.edit()
+ editor.putString(getString(R.string.ACCESS_TOKEN), StringUtils.trimToNull(apiToken))
+ editor.apply()
+ }
+ var stationFilter: StationFilter
+ get() {
+ val photoFilter = getOptionalBoolean(R.string.STATION_FILTER_PHOTO)
+ val activeFilter = getOptionalBoolean(R.string.STATION_FILTER_ACTIVE)
+ val nicknameFilter =
+ preferences.getString(getString(R.string.STATION_FILTER_NICKNAME), null)
+ return StationFilter(photoFilter, activeFilter, nicknameFilter)
+ }
+ set(stationFilter) {
+ putString(
+ R.string.STATION_FILTER_PHOTO,
+ if (stationFilter.hasPhoto() == null) null else stationFilter.hasPhoto()
+ .toString()
+ )
+ putString(
+ R.string.STATION_FILTER_ACTIVE,
+ if (stationFilter.isActive == null) null else stationFilter.isActive.toString()
+ )
+ putString(R.string.STATION_FILTER_NICKNAME, stationFilter.nickname)
+ }
+
+ private fun getOptionalBoolean(key: Int): Boolean? {
+ return if (preferences.contains(getString(key))) {
+ java.lang.Boolean.valueOf(preferences.getString(getString(key), "false"))
+ } else null
+ }
+
+ var lastUpdate: Long
+ get() = preferences.getLong(getString(R.string.LAST_UPDATE), 0L)
+ set(lastUpdate) {
+ putLong(R.string.LAST_UPDATE, lastUpdate)
+ }
+ var isLocationUpdates: Boolean
+ get() = preferences.getBoolean(getString(R.string.LOCATION_UPDATES), true)
+ set(locationUpdates) {
+ putBoolean(R.string.LOCATION_UPDATES, locationUpdates)
+ }
+ var lastMapPosition: MapPosition
+ get() {
+ val latLong = LatLong(
+ getDouble(R.string.LAST_POSITION_LAT),
+ getDouble(R.string.LAST_POSITION_LON)
+ )
+ return MapPosition(
+ latLong,
+ preferences.getLong(
+ getString(R.string.LAST_POSITION_ZOOM),
+ zoomLevelDefault.toLong()
+ ).toByte()
+ )
+ }
+ set(lastMapPosition) {
+ putDouble(R.string.LAST_POSITION_LAT, lastMapPosition.latLong.latitude)
+ putDouble(R.string.LAST_POSITION_LON, lastMapPosition.latLong.longitude)
+ putLong(R.string.LAST_POSITION_ZOOM, lastMapPosition.zoomLevel.toLong())
+ }
+ val lastLocation: Location
+ get() {
+ val location = Location("")
+ location.latitude = getDouble(R.string.LAST_POSITION_LAT)
+ location.longitude = getDouble(R.string.LAST_POSITION_LON)
+ return location
+ }
+ val zoomLevelDefault: Byte
+ /**
+ * @return the default starting zoom level if nothing is encoded in the map file.
+ */
+ get() = 12.toByte()
+ var anonymous: Boolean
+ get() = preferences.getBoolean(getString(R.string.ANONYMOUS), false)
+ set(anonymous) {
+ putBoolean(R.string.ANONYMOUS, anonymous)
+ }
+ var profile: Profile
+ get() = Profile(
+ nickname,
+ license,
+ photoOwner,
+ anonymous,
+ photographerLink,
+ email,
+ isEmailVerified
+ )
+ set(profile) {
+ license = profile.license
+ photoOwner = profile.photoOwner
+ anonymous = profile.anonymous
+ photographerLink = profile.link
+ nickname = profile.nickname
+ email = profile.email
+ isEmailVerified = profile.emailVerified
+ }
+ var map: String?
+ get() = preferences.getString(getString(R.string.MAP_FILE), null)
+ set(map) {
+ putString(R.string.MAP_FILE, map)
+ }
+
+ private fun putUri(key: Int, uri: Uri?) {
+ putString(key, uri?.toString())
+ }
+
+ val mapDirectoryUri: Uri?
+ get() = getUri(getString(R.string.MAP_DIRECTORY))
+
+ private fun getUri(key: String): Uri? {
+ return toUri(preferences.getString(key, null))
+ }
+
+ fun setMapDirectoryUri(mapDirectory: Uri?) {
+ putUri(R.string.MAP_DIRECTORY, mapDirectory)
+ }
+
+ val mapThemeDirectoryUri: Uri?
+ get() = getUri(getString(R.string.MAP_THEME_DIRECTORY))
+
+ fun setMapThemeDirectoryUri(mapThemeDirectory: Uri?) {
+ putUri(R.string.MAP_THEME_DIRECTORY, mapThemeDirectory)
+ }
+
+ val mapThemeUri: Uri?
+ get() = getUri(getString(R.string.MAP_THEME))
+
+ fun setMapThemeUri(mapTheme: Uri?) {
+ putUri(R.string.MAP_THEME, mapTheme)
+ }
+
+ var sortByDistance: Boolean
+ get() = preferences.getBoolean(getString(R.string.SORT_BY_DISTANCE), false)
+ set(sortByDistance) {
+ putBoolean(R.string.SORT_BY_DISTANCE, sortByDistance)
+ }
+ val isLoggedIn: Boolean
+ get() = rsapiClient.hasToken()
+
+ companion object {
+ private val TAG = BaseApplication::class.java.simpleName
+ private const val DEFAULT_FIRSTAPPSTART = false
+ private const val DEFAULT = ""
+ lateinit var instance: BaseApplication
+ private set
+ const val DEFAULT_COUNTRY = "de"
+ const val PREF_FILE = "APP_PREF_FILE"
+
+ private fun getValidatedApiUrlString(apiUrl: String?): String {
+ val uri = toUri(apiUrl)
+ uri?.let {
+ val scheme = it.scheme
+ if (scheme != null && scheme.matches("https?".toRegex())) {
+ return apiUrl + if (apiUrl!!.endsWith("/")) "" else "/"
+ }
+ }
+ return "https://api.railway-stations.org/"
+ }
+
+ fun toUri(uriString: String?): Uri? {
+ try {
+ return Uri.parse(uriString)
+ } catch (ignored: Exception) {
+ Log.e(TAG, "can't read Uri string $uriString")
+ }
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/CountryActivity.kt b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/CountryActivity.kt
new file mode 100644
index 00000000..16d4622b
--- /dev/null
+++ b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/CountryActivity.kt
@@ -0,0 +1,58 @@
+package de.bahnhoefe.deutschlands.bahnhofsfotos
+
+import android.os.Bundle
+import android.view.MenuItem
+import android.view.View
+import android.widget.AdapterView
+import android.widget.AdapterView.OnItemClickListener
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.app.AppCompatActivity
+import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityCountryBinding
+import de.bahnhoefe.deutschlands.bahnhofsfotos.db.CountryAdapter
+
+class CountryActivity : AppCompatActivity() {
+ private lateinit var countryAdapter: CountryAdapter
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val binding = ActivityCountryBinding.inflate(
+ layoutInflater
+ )
+ setContentView(binding.root)
+ val cursor = (application as BaseApplication).dbAdapter.countryList
+ countryAdapter = CountryAdapter(this, cursor, 0)
+ binding.lstCountries.adapter = countryAdapter
+ binding.lstCountries.onItemClickListener =
+ OnItemClickListener { _: AdapterView<*>?, view: View?, position: Int, _: Long ->
+ countryAdapter.getView(
+ position,
+ view,
+ binding.lstCountries,
+ cursor
+ )
+ }
+
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ navigateUp()
+ }
+ })
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == R.id.home) {
+ navigateUp()
+ return true
+ }
+ return false
+ }
+
+ private fun navigateUp() {
+ val baseApplication: BaseApplication = BaseApplication.instance
+ val selectedCountries = countryAdapter.selectedCountries
+ if (baseApplication.countryCodes != selectedCountries) {
+ baseApplication.countryCodes = selectedCountries
+ baseApplication.lastUpdate = 0L
+ }
+ finish()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/DetailsActivity.kt b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/DetailsActivity.kt
new file mode 100644
index 00000000..a2a9febd
--- /dev/null
+++ b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/DetailsActivity.kt
@@ -0,0 +1,574 @@
+package de.bahnhoefe.deutschlands.bahnhofsfotos
+
+import android.app.TaskStackBuilder
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.text.Html
+import android.text.TextUtils
+import android.text.method.LinkMovementMethod
+import android.util.Log
+import android.view.ContextThemeWrapper
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.ImageView
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback
+import androidx.core.app.NavUtils
+import androidx.core.content.ContextCompat
+import androidx.core.content.FileProvider
+import androidx.viewpager2.widget.ViewPager2
+import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityDetailsBinding
+import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.StationInfoBinding
+import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country.Companion.getCountryByCode
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PageablePhoto
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Photo
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PhotoStation
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PhotoStations
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.ProviderApp
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Upload
+import de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi.RSAPIClient
+import de.bahnhoefe.deutschlands.bahnhofsfotos.util.BitmapCache
+import de.bahnhoefe.deutschlands.bahnhofsfotos.util.ConnectionUtil
+import de.bahnhoefe.deutschlands.bahnhofsfotos.util.Constants
+import de.bahnhoefe.deutschlands.bahnhofsfotos.util.FileUtils
+import de.bahnhoefe.deutschlands.bahnhofsfotos.util.NavItem
+import de.bahnhoefe.deutschlands.bahnhofsfotos.util.Timetable
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.util.Locale
+import java.util.function.Consumer
+
+class DetailsActivity : AppCompatActivity(), OnRequestPermissionsResultCallback {
+ private lateinit var baseApplication: BaseApplication
+ private lateinit var rsapiClient: RSAPIClient
+ private lateinit var binding: ActivityDetailsBinding
+ private lateinit var station: Station
+ private lateinit var countries: Set
+ private var nickname: String? = null
+ private lateinit var photoPagerAdapter: PhotoPagerAdapter
+ private val photoBitmaps: MutableMap = mutableMapOf()
+ private var selectedPhoto: PageablePhoto? = null
+ private val carouselPageIndicators: MutableList = mutableListOf()
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityDetailsBinding.inflate(
+ layoutInflater
+ )
+ setContentView(binding.root)
+ baseApplication = application as BaseApplication
+ rsapiClient = baseApplication.rsapiClient
+ countries = baseApplication.dbAdapter
+ .fetchCountriesWithProviderApps(baseApplication.countryCodes)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ photoPagerAdapter = PhotoPagerAdapter(this)
+ binding.details.viewPager.adapter = photoPagerAdapter
+ binding.details.viewPager.setCurrentItem(0, false)
+ binding.details.viewPager.registerOnPageChangeCallback(object :
+ ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ val pageablePhoto = photoPagerAdapter.getPageablePhotoAtPosition(position)
+ onPageablePhotoSelected(pageablePhoto, position)
+ }
+ })
+
+ // switch off image and license view until we actually have a foto
+ binding.details.licenseTag.visibility = View.INVISIBLE
+ binding.details.licenseTag.movementMethod = LinkMovementMethod.getInstance()
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ navigateUp()
+ }
+ })
+ binding.details.marker.setOnClickListener { showStationInfo() }
+ readPreferences()
+ onNewIntent(intent)
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ val newStation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getSerializableExtra(EXTRA_STATION, Station::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getSerializableExtra(EXTRA_STATION) as Station?
+ }
+ if (newStation == null) {
+ Log.w(TAG, "EXTRA_STATION in intent data missing")
+ Toast.makeText(this, R.string.station_not_found, Toast.LENGTH_LONG).show()
+ finish()
+ return
+ }
+ station = newStation
+ binding.details.marker.setImageDrawable(ContextCompat.getDrawable(this, markerRes))
+ binding.details.tvStationTitle.text = station.title
+ binding.details.tvStationTitle.isSingleLine = false
+ station.photoUrl?.let {
+ if (ConnectionUtil.checkInternetConnection(this)) {
+ photoBitmaps[it] = null
+ BitmapCache.instance
+ ?.getPhoto(it) { bitmap: Bitmap? ->
+ if (bitmap != null) {
+ val pageablePhoto = PageablePhoto(station, bitmap)
+ runOnUiThread {
+ addIndicator()
+ val position =
+ photoPagerAdapter.addPageablePhoto(pageablePhoto)
+ if (position == 0) {
+ onPageablePhotoSelected(pageablePhoto, position)
+ }
+ }
+ }
+ }
+ }
+ }
+ loadAdditionalPhotos(station)
+ baseApplication.dbAdapter
+ .getPendingUploadsForStation(station)
+ .forEach(Consumer { upload: Upload? -> addUploadPhoto(upload) })
+ }
+
+ private fun addUploadPhoto(upload: Upload?) {
+ if (!upload!!.isPendingPhotoUpload) {
+ return
+ }
+ val profile = baseApplication.profile
+ val file = FileUtils.getStoredMediaFile(this, upload.id)
+ if (file != null && file.canRead()) {
+ val bitmap = BitmapFactory.decodeFile(file.absolutePath)
+ if (bitmap != null) {
+ val pageablePhoto = PageablePhoto(
+ upload.id!!,
+ file.toURI().toString(),
+ getString(R.string.new_local_photo),
+ "",
+ if (profile.license != null) profile.license!!.longName else "",
+ "",
+ bitmap
+ )
+ runOnUiThread {
+ addIndicator()
+ val position = photoPagerAdapter.addPageablePhoto(pageablePhoto)
+ if (position == 0) {
+ onPageablePhotoSelected(pageablePhoto, position)
+ }
+ }
+ }
+ }
+ }
+
+ private fun loadAdditionalPhotos(station: Station) {
+ rsapiClient.getPhotoStationById(station.country, station.id)
+ .enqueue(object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response
+ ) {
+ if (response.isSuccessful) {
+ val photoStations = response.body() ?: return
+ photoStations.stations
+ .flatMap { (_, _, _, _, _, _, _, photos): PhotoStation -> photos }
+ .forEach { photo: Photo ->
+ val url = photoStations.photoBaseUrl + photo.path
+ if (!photoBitmaps.containsKey(url)) {
+ photoBitmaps[url] = null
+ addIndicator()
+ BitmapCache.instance
+ ?.getPhoto(url) { fetchedBitmap: Bitmap? ->
+ runOnUiThread {
+ addAdditionalPhotoToPagerAdapter(
+ photo,
+ url,
+ photoStations,
+ fetchedBitmap
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ Log.e(TAG, "Failed to load additional photos", t)
+ }
+ })
+ }
+
+ private fun addAdditionalPhotoToPagerAdapter(
+ photo: Photo,
+ url: String,
+ photoStations: PhotoStations,
+ bitmap: Bitmap?
+ ) {
+ bitmap?.let {
+ photoPagerAdapter.addPageablePhoto(
+ PageablePhoto(
+ photo.id,
+ url,
+ photo.photographer,
+ photoStations.getPhotographerUrl(photo.photographer),
+ photoStations.getLicenseName(photo.license),
+ photoStations.getLicenseUrl(photo.license),
+ it
+ )
+ )
+ }
+ }
+
+ private fun addIndicator() {
+ val indicator = ImageView(this@DetailsActivity)
+ indicator.setImageResource(R.drawable.selector_carousel_page_indicator)
+ indicator.setPadding(0, 0, 5, 0) // left, top, right, bottom
+ binding.details.llPageIndicatorContainer.addView(indicator)
+ carouselPageIndicators.add(indicator)
+ }
+
+ private val markerRes: Int
+ get() {
+ return if (station.hasPhoto()) {
+ if (isOwner) {
+ if (station.active) R.drawable.marker_violet else R.drawable.marker_violet_inactive
+ } else {
+ if (station.active) R.drawable.marker_green else R.drawable.marker_green_inactive
+ }
+ } else {
+ if (station.active) R.drawable.marker_red else R.drawable.marker_red_inactive
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ readPreferences()
+ }
+
+ private fun readPreferences() {
+ nickname = baseApplication.nickname
+ }
+
+ private val isOwner: Boolean
+ get() = TextUtils.equals(nickname, station.photographer)
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.details, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ private var activityForResultLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { _: ActivityResult? -> recreate() }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.add_photo -> {
+ val intent = Intent(this@DetailsActivity, UploadActivity::class.java)
+ intent.putExtra(UploadActivity.EXTRA_STATION, station)
+ activityForResultLauncher.launch(intent)
+ }
+
+ R.id.report_problem -> {
+ val intent = Intent(this@DetailsActivity, ProblemReportActivity::class.java)
+ intent.putExtra(ProblemReportActivity.EXTRA_STATION, station)
+ intent.putExtra(
+ ProblemReportActivity.EXTRA_PHOTO_ID,
+ if (selectedPhoto != null) selectedPhoto!!.id else null
+ )
+ startActivity(intent)
+ }
+
+ R.id.nav_to_station -> {
+ startNavigation(this@DetailsActivity)
+ }
+
+ R.id.timetable -> {
+ getCountryByCode(countries, station.country)?.let { country: Country ->
+ Timetable().createTimetableIntent(country, station)?.let { startActivity(it) }
+ }
+ }
+
+ R.id.share_link -> {
+ val stationUri = Uri.parse(
+ String.format(
+ "https://map.railway-stations.org/station.php?countryCode=%s&stationId=%s",
+ station.country,
+ station.id
+ )
+ )
+ startActivity(Intent(Intent.ACTION_VIEW, stationUri))
+ }
+
+ R.id.share_photo -> {
+ createPhotoSendIntent()?.let {
+ it.putExtra(Intent.EXTRA_TEXT, binding.details.tvStationTitle.text)
+ it.type = "image/jpeg"
+ startActivity(Intent.createChooser(it, "send"))
+ }
+ }
+
+ R.id.station_info -> {
+ showStationInfo()
+ }
+
+ R.id.provider_android_app -> {
+ getCountryByCode(countries, station.country)?.let { country: Country ->
+ val providerApps = country.compatibleProviderApps
+ if (providerApps.size == 1) {
+ openAppOrPlayStore(providerApps[0], this)
+ } else if (providerApps.size > 1) {
+ val appNames = providerApps
+ .map(ProviderApp::name)
+ .toTypedArray()
+ SimpleDialogs.simpleSelect(
+ this,
+ resources.getString(R.string.choose_provider_app),
+ appNames
+ ) { _: DialogInterface?, which: Int ->
+ if (which >= 0 && providerApps.size > which) {
+ openAppOrPlayStore(providerApps[which], this@DetailsActivity)
+ }
+ }
+ } else {
+ Toast.makeText(this, R.string.provider_app_missing, Toast.LENGTH_LONG)
+ .show()
+ }
+ }
+ }
+
+ android.R.id.home -> {
+ navigateUp()
+ }
+
+ else -> {
+ return super.onOptionsItemSelected(item)
+ }
+ }
+ return true
+ }
+
+ /**
+ * Tries to open the provider app if installed. If it is not installed or cannot be opened Google Play Store will be opened instead.
+ *
+ * @param context activity context
+ */
+ private fun openAppOrPlayStore(providerApp: ProviderApp, context: Context) {
+ // Try to open App
+ val success = openApp(providerApp, context)
+ // Could not open App, open play store instead
+ if (!success) {
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = Uri.parse(providerApp.url)
+ context.startActivity(intent)
+ }
+ }
+
+ /**
+ * Open another app.
+ * https://stackoverflow.com/a/7596063/714965
+ *
+ * @param context activity context
+ * @return true if likely successful, false if unsuccessful
+ */
+ private fun openApp(providerApp: ProviderApp, context: Context): Boolean {
+ if (!providerApp.isAndroid) {
+ return false
+ }
+ return try {
+ val packageName = Uri.parse(providerApp.url).getQueryParameter("id")!!
+ val intent =
+ context.packageManager.getLaunchIntentForPackage(packageName) ?: return false
+ intent.addCategory(Intent.CATEGORY_LAUNCHER)
+ context.startActivity(intent)
+ true
+ } catch (e: ActivityNotFoundException) {
+ false
+ }
+ }
+
+ fun navigateUp() {
+ val callingActivity =
+ callingActivity // if MapsActivity was calling, then we don't want to rebuild the Backstack
+ val upIntent = NavUtils.getParentActivityIntent(this)
+ if (callingActivity == null && upIntent != null) {
+ upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
+ if (NavUtils.shouldUpRecreateTask(this, upIntent) || isTaskRoot) {
+ Log.v(TAG, "Recreate back stack")
+ TaskStackBuilder.create(this).addNextIntentWithParentStack(upIntent)
+ .startActivities()
+ }
+ }
+ finish()
+ }
+
+ private fun showStationInfo() {
+ val stationInfoBinding = StationInfoBinding.inflate(
+ layoutInflater
+ )
+ stationInfoBinding.id.text = station.id
+ stationInfoBinding.coordinates.text = String.format(
+ Locale.US,
+ resources.getString(R.string.coordinates),
+ station.lat,
+ station.lon
+ )
+ stationInfoBinding.active.setText(if (station.active) R.string.active else R.string.inactive)
+ stationInfoBinding.owner.text =
+ if (station.photographer != null) station.photographer else ""
+ if (station.outdated) {
+ stationInfoBinding.outdatedLabel.visibility = View.VISIBLE
+ }
+ AlertDialog.Builder(ContextThemeWrapper(this, R.style.AlertDialogCustom))
+ .setTitle(binding.details.tvStationTitle.text)
+ .setView(stationInfoBinding.root)
+ .setIcon(R.mipmap.ic_launcher)
+ .setPositiveButton(android.R.string.ok, null)
+ .create()
+ .show()
+ }
+
+ private fun createPhotoSendIntent(): Intent? {
+ if (selectedPhoto != null) {
+ val sendIntent = Intent(Intent.ACTION_SEND)
+ val newFile = FileUtils.getImageCacheFile(
+ applicationContext, System.currentTimeMillis().toString()
+ )
+ try {
+ Log.i(TAG, "Save photo to: $newFile")
+ selectedPhoto!!.bitmap!!.compress(
+ Bitmap.CompressFormat.JPEG,
+ Constants.STORED_PHOTO_QUALITY,
+ FileOutputStream(newFile)
+ )
+ sendIntent.putExtra(
+ Intent.EXTRA_STREAM, FileProvider.getUriForFile(
+ this@DetailsActivity,
+ BuildConfig.APPLICATION_ID + ".fileprovider", newFile
+ )
+ )
+ return sendIntent
+ } catch (e: FileNotFoundException) {
+ Log.e(TAG, "Error saving cached bitmap", e)
+ }
+ }
+ return null
+ }
+
+ private fun startNavigation(context: Context) {
+ val adapter: ArrayAdapter = object : ArrayAdapter(
+ this, android.R.layout.select_dialog_item,
+ android.R.id.text1, NavItem.values()
+ ) {
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val item = getItem(position)!!
+ val view = super.getView(position, convertView, parent)
+ val tv = view.findViewById(android.R.id.text1)
+
+ //Put the image on the TextView
+ tv.setCompoundDrawablesWithIntrinsicBounds(item.iconRes, 0, 0, 0)
+ tv.text = getString(item.textRes)
+
+ //Add margin between image and text (support various screen densities)
+ val dp5 = (20 * resources.displayMetrics.density + 0.5f).toInt()
+ val dp7 = (20 * resources.displayMetrics.density).toInt()
+ tv.compoundDrawablePadding = dp5
+ tv.setPadding(dp7, 0, 0, 0)
+ return view
+ }
+ }
+ AlertDialog.Builder(this)
+ .setIcon(R.mipmap.ic_launcher)
+ .setTitle(R.string.navMethod)
+ .setAdapter(adapter) { _: DialogInterface?, position: Int ->
+ val item = adapter.getItem(position)!!
+ val lat = station.lat
+ val lon = station.lon
+ val intent = item.createIntent(
+ this@DetailsActivity,
+ lat,
+ lon,
+ binding.details.tvStationTitle.text.toString(),
+ markerRes
+ )
+ try {
+ startActivity(intent)
+ } catch (e: Exception) {
+ Toast.makeText(context, R.string.activitynotfound, Toast.LENGTH_LONG).show()
+ }
+ }.show()
+ }
+
+ fun onPageablePhotoSelected(pageablePhoto: PageablePhoto?, position: Int) {
+ selectedPhoto = pageablePhoto
+ binding.details.licenseTag.visibility = View.INVISIBLE
+ if (pageablePhoto == null) {
+ return
+ }
+
+ // Lizenzinfo aufbauen und einblenden
+ binding.details.licenseTag.visibility = View.VISIBLE
+ val photographerUrlAvailable =
+ pageablePhoto.photographerUrl != null && pageablePhoto.photographerUrl!!.isNotEmpty()
+ val licenseUrlAvailable =
+ pageablePhoto.licenseUrl != null && pageablePhoto.licenseUrl!!.isNotEmpty()
+ val photographerText: String? = if (photographerUrlAvailable) {
+ String.format(
+ LINK_FORMAT,
+ pageablePhoto.photographerUrl,
+ pageablePhoto.photographer
+ )
+ } else {
+ pageablePhoto.photographer
+ }
+ val licenseText: String? = if (licenseUrlAvailable) {
+ String.format(
+ LINK_FORMAT,
+ pageablePhoto.licenseUrl,
+ pageablePhoto.license
+ )
+ } else {
+ pageablePhoto.license
+ }
+ binding.details.licenseTag.text = Html.fromHtml(
+ String.format(
+ getText(R.string.license_tag).toString(),
+ photographerText,
+ licenseText
+ ), Html.FROM_HTML_MODE_LEGACY
+ )
+ for (i in carouselPageIndicators.indices) {
+ if (i == position) {
+ carouselPageIndicators[position].isSelected = true
+ } else {
+ carouselPageIndicators[i].isSelected = false
+ }
+ }
+ }
+
+ companion object {
+ private val TAG = DetailsActivity::class.java.simpleName
+
+ // Names of Extras that this class reacts to
+ const val EXTRA_STATION = "EXTRA_STATION"
+ private const val LINK_FORMAT = "%s"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/HighScoreActivity.kt b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/HighScoreActivity.kt
new file mode 100644
index 00000000..8812d8ea
--- /dev/null
+++ b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/HighScoreActivity.kt
@@ -0,0 +1,144 @@
+package de.bahnhoefe.deutschlands.bahnhofsfotos
+
+import android.app.SearchManager
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.view.Menu
+import android.view.View
+import android.widget.AdapterView
+import android.widget.AdapterView.OnItemClickListener
+import android.widget.ArrayAdapter
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.SearchView
+import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityHighScoreBinding
+import de.bahnhoefe.deutschlands.bahnhofsfotos.db.HighScoreAdapter
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Country
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.HighScore
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.HighScoreItem
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+
+class HighScoreActivity : AppCompatActivity() {
+ private var adapter: HighScoreAdapter? = null
+ private lateinit var binding: ActivityHighScoreBinding
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityHighScoreBinding.inflate(
+ layoutInflater
+ )
+ setContentView(binding.root)
+ val baseApplication = application as BaseApplication
+ val firstSelectedCountry = baseApplication.countryCodes.iterator().next()
+ val countries = ArrayList(baseApplication.dbAdapter.allCountries)
+ countries.sort()
+ countries.add(0, Country("", getString(R.string.all_countries)))
+ var selectedItem = 0
+ for (country in countries) {
+ if (country!!.code == firstSelectedCountry) {
+ selectedItem = countries.indexOf(country)
+ }
+ }
+ val countryAdapter = ArrayAdapter(
+ this,
+ android.R.layout.simple_spinner_dropdown_item,
+ countries.toTypedArray()
+ )
+ binding.countries.adapter = countryAdapter
+ binding.countries.setSelection(selectedItem)
+ binding.countries.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(
+ parent: AdapterView<*>,
+ view: View,
+ position: Int,
+ id: Long
+ ) {
+ loadHighScore(baseApplication, parent.selectedItem as Country)
+ }
+
+ override fun onNothingSelected(parent: AdapterView<*>?) {}
+ }
+ }
+
+ private fun loadHighScore(baseApplication: BaseApplication, selectedCountry: Country) {
+ val rsapi = baseApplication.rsapiClient
+ val highScoreCall =
+ if (selectedCountry.code.isEmpty()) rsapi.getHighScore() else rsapi.getHighScore(
+ selectedCountry.code
+ )
+ highScoreCall.enqueue(object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ if (response.isSuccessful) {
+ adapter = HighScoreAdapter(this@HighScoreActivity, response.body()!!.getItems())
+ binding.highscoreList.adapter = adapter
+ binding.highscoreList.onItemClickListener =
+ OnItemClickListener { adapter: AdapterView<*>, _: View?, position: Int, _: Long ->
+ val (name) = adapter.getItemAtPosition(position) as HighScoreItem
+ val stationFilter = baseApplication.stationFilter
+ stationFilter.nickname = name
+ baseApplication.stationFilter = stationFilter
+ val intent = Intent(this@HighScoreActivity, MapsActivity::class.java)
+ startActivity(intent)
+ }
+ }
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ Log.e(TAG, "Error loading highscore", t)
+ Toast.makeText(
+ baseContext,
+ getString(R.string.error_loading_highscore) + t.message,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ })
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_high_score, menu)
+ val manager = getSystemService(SEARCH_SERVICE) as SearchManager
+ val search = menu.findItem(R.id.search).actionView as SearchView?
+ search!!.setSearchableInfo(manager.getSearchableInfo(componentName))
+ search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(s: String): Boolean {
+ Log.d(TAG, "onQueryTextSubmit ")
+ if (adapter != null) {
+ adapter!!.filter.filter(s)
+ if (adapter!!.isEmpty) {
+ Toast.makeText(
+ this@HighScoreActivity,
+ R.string.no_records_found,
+ Toast.LENGTH_LONG
+ ).show()
+ } else {
+ Toast.makeText(
+ this@HighScoreActivity,
+ resources.getQuantityString(
+ R.plurals.records_found,
+ adapter!!.count,
+ adapter!!.count
+ ),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ return false
+ }
+
+ override fun onQueryTextChange(s: String): Boolean {
+ Log.d(TAG, "onQueryTextChange ")
+ if (adapter != null) {
+ adapter!!.filter.filter(s)
+ }
+ return false
+ }
+ })
+ return true
+ }
+
+ companion object {
+ private const val TAG = "HighScoreActivity"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/InboxActivity.kt b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/InboxActivity.kt
new file mode 100644
index 00000000..1d3177be
--- /dev/null
+++ b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/InboxActivity.kt
@@ -0,0 +1,65 @@
+package de.bahnhoefe.deutschlands.bahnhofsfotos
+
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import android.widget.AdapterView
+import android.widget.AdapterView.OnItemClickListener
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityInboxBinding
+import de.bahnhoefe.deutschlands.bahnhofsfotos.db.InboxAdapter
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.PublicInbox
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+
+class InboxActivity : AppCompatActivity() {
+ private var adapter: InboxAdapter? = null
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val binding = ActivityInboxBinding.inflate(
+ layoutInflater
+ )
+ setContentView(binding.root)
+ val inboxCall = (application as BaseApplication).rsapiClient.getPublicInbox()
+ inboxCall.enqueue(object : Callback> {
+ override fun onResponse(
+ call: Call>,
+ response: Response>
+ ) {
+ val body = response.body()
+ if (response.isSuccessful && body != null) {
+ adapter = InboxAdapter(this@InboxActivity, body)
+ binding.inboxList.adapter = adapter
+ binding.inboxList.onItemClickListener =
+ OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
+ val (_, _, stationId, lat, lon) = body[position]
+ val intent = Intent(this@InboxActivity, MapsActivity::class.java)
+ intent.putExtra(MapsActivity.EXTRAS_LATITUDE, lat)
+ intent.putExtra(MapsActivity.EXTRAS_LONGITUDE, lon)
+ intent.putExtra(
+ MapsActivity.EXTRAS_MARKER,
+ if (stationId == null) R.drawable.marker_missing else R.drawable.marker_red
+ )
+ startActivity(intent)
+ }
+ }
+ }
+
+ override fun onFailure(call: Call>, t: Throwable) {
+ Log.e(TAG, "Error loading public inbox", t)
+ Toast.makeText(
+ baseContext,
+ getString(R.string.error_loading_inbox) + t.message,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ })
+ }
+
+ companion object {
+ private val TAG = InboxActivity::class.java.simpleName
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/IntroSliderActivity.kt b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/IntroSliderActivity.kt
new file mode 100644
index 00000000..4e0f08fe
--- /dev/null
+++ b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/IntroSliderActivity.kt
@@ -0,0 +1,144 @@
+package de.bahnhoefe.deutschlands.bahnhofsfotos
+
+import android.content.Intent
+import android.graphics.Color
+import android.os.Build
+import android.os.Bundle
+import android.text.Html
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowInsets
+import android.view.WindowInsetsController
+import android.view.WindowManager
+import android.widget.TextView
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.app.AppCompatActivity
+import androidx.viewpager.widget.PagerAdapter
+import androidx.viewpager.widget.ViewPager.OnPageChangeListener
+import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityIntroSliderBinding
+
+
+class IntroSliderActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityIntroSliderBinding
+ private lateinit var layouts: IntArray
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val baseApplication = application as BaseApplication
+ binding = ActivityIntroSliderBinding.inflate(layoutInflater)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ window.setDecorFitsSystemWindows(false)
+ window.decorView.windowInsetsController?.let {
+ it.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
+ it.systemBarsBehavior =
+ WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+ } else {
+ @Suppress("DEPRECATION")
+ window.decorView.systemUiVisibility =
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ }
+ setContentView(binding.root)
+ layouts = intArrayOf(R.layout.intro_slider1, R.layout.intro_slider2)
+ addBottomDots(0)
+ changeStatusBarColor()
+ val viewPagerAdapter = ViewPagerAdapter()
+ binding.viewPager.adapter = viewPagerAdapter
+ binding.viewPager.addOnPageChangeListener(viewListener)
+ binding.btnSliderSkip.setOnClickListener {
+ baseApplication.firstAppStart = true
+ openMainActivity()
+ }
+ binding.btnSliderNext.setOnClickListener {
+ val current = nextItem
+ if (current < layouts.size) {
+ binding.viewPager.currentItem = current
+ } else {
+ openMainActivity()
+ }
+ }
+
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ openMainActivity()
+ }
+ })
+ }
+
+ private fun openMainActivity() {
+ startActivity(Intent(this@IntroSliderActivity, MainActivity::class.java))
+ finish()
+ }
+
+ private fun addBottomDots(position: Int) {
+ val dots = arrayOfNulls(layouts.size)
+ val colorActive = resources.getIntArray(R.array.dot_active)
+ val colorInactive = resources.getIntArray(R.array.dot_inactive)
+ binding.layoutDots.removeAllViews()
+ dots.indices.forEach { i ->
+ val textView = TextView(this)
+ textView.text = Html.fromHtml("•", Html.FROM_HTML_MODE_LEGACY)
+ textView.textSize = 35f
+ textView.setTextColor(colorInactive[position])
+ dots[i] = textView
+ binding.layoutDots.addView(textView)
+ }
+ if (dots.isNotEmpty()) {
+ dots[position]!!.setTextColor(colorActive[position])
+ }
+ }
+
+ private val nextItem: Int
+ get() = binding.viewPager.currentItem + 1
+ private val viewListener: OnPageChangeListener = object : OnPageChangeListener {
+ override fun onPageScrolled(
+ position: Int,
+ positionOffset: Float,
+ positionOffsetPixels: Int
+ ) {
+ }
+
+ override fun onPageSelected(position: Int) {
+ val baseApplication = application as BaseApplication
+ addBottomDots(position)
+ if (position == layouts.size - 1) {
+ binding.btnSliderNext.setText(R.string.proceed)
+ binding.btnSliderSkip.visibility = View.INVISIBLE
+ baseApplication.firstAppStart = true
+ } else {
+ binding.btnSliderNext.setText(R.string.next)
+ binding.btnSliderSkip.visibility = View.VISIBLE
+ }
+ }
+
+ override fun onPageScrollStateChanged(state: Int) {}
+ }
+
+ private fun changeStatusBarColor() {
+ val window = window
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ window.statusBarColor = Color.TRANSPARENT
+ }
+
+ inner class ViewPagerAdapter : PagerAdapter() {
+ override fun instantiateItem(container: ViewGroup, position: Int): Any {
+ val layoutInflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
+ val view = layoutInflater.inflate(layouts[position], container, false)
+ container.addView(view)
+ return view
+ }
+
+ override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
+ val view = `object` as View
+ container.removeView(view)
+ }
+
+ override fun getCount(): Int {
+ return layouts.size
+ }
+
+ override fun isViewFromObject(view: View, `object`: Any): Boolean {
+ return view === `object`
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/MainActivity.kt b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/MainActivity.kt
new file mode 100644
index 00000000..962128d2
--- /dev/null
+++ b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/MainActivity.kt
@@ -0,0 +1,558 @@
+package de.bahnhoefe.deutschlands.bahnhofsfotos
+
+import android.Manifest
+import android.app.AlertDialog
+import android.app.SearchManager
+import android.content.ComponentName
+import android.content.DialogInterface
+import android.content.Intent
+import android.content.ServiceConnection
+import android.content.pm.PackageManager
+import android.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.IBinder
+import android.util.Log
+import android.view.ContextThemeWrapper
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.AdapterView.OnItemClickListener
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.ActionBarDrawerToggle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.SearchView
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.core.view.GravityCompat
+import com.google.android.material.navigation.NavigationView
+import de.bahnhoefe.deutschlands.bahnhofsfotos.NearbyNotificationService.StatusBinder
+import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityMainBinding
+import de.bahnhoefe.deutschlands.bahnhofsfotos.db.DbAdapter
+import de.bahnhoefe.deutschlands.bahnhofsfotos.db.StationListAdapter
+import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.AppInfoFragment
+import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs
+import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.StationFilterBar
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Statistic
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.UpdatePolicy
+import de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi.RSAPIClient
+import de.bahnhoefe.deutschlands.bahnhofsfotos.util.StationFilter
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import java.text.SimpleDateFormat
+
+class MainActivity : AppCompatActivity(), LocationListener,
+ NavigationView.OnNavigationItemSelectedListener, StationFilterBar.OnChangeListener {
+ private lateinit var baseApplication: BaseApplication
+ private lateinit var dbAdapter: DbAdapter
+ private lateinit var binding: ActivityMainBinding
+ private var stationListAdapter: StationListAdapter? = null
+ private var searchString: String? = null
+ private var statusBinder: StatusBinder? = null
+ private lateinit var rsapiClient: RSAPIClient
+ private var myPos: Location? = null
+ private var locationManager: LocationManager? = null
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(
+ layoutInflater
+ )
+ setContentView(binding.root)
+ setSupportActionBar(binding.appBarMain.toolbar)
+ baseApplication = application as BaseApplication
+ dbAdapter = baseApplication.dbAdapter
+ rsapiClient = baseApplication.rsapiClient
+ val toggle = ActionBarDrawerToggle(
+ this,
+ binding.drawerLayout,
+ binding.appBarMain.toolbar,
+ R.string.navigation_drawer_open,
+ R.string.navigation_drawer_close
+ )
+ binding.drawerLayout.addDrawerListener(toggle)
+ toggle.syncState()
+ binding.navView.setNavigationItemSelectedListener(this)
+ val header = binding.navView.getHeaderView(0)
+ val tvUpdate = header.findViewById(R.id.tvUpdate)
+ if (!baseApplication.firstAppStart) {
+ startActivity(Intent(this, IntroSliderActivity::class.java))
+ finish()
+ }
+ val lastUpdateDate = baseApplication.lastUpdate
+ if (lastUpdateDate > 0) {
+ tvUpdate.text = getString(
+ R.string.last_update_at,
+ SimpleDateFormat.getDateTimeInstance().format(lastUpdateDate)
+ )
+ } else {
+ tvUpdate.setText(R.string.no_stations_in_database)
+ }
+ val searchIntent = intent
+ if (Intent.ACTION_SEARCH == searchIntent.action) {
+ searchString = searchIntent.getStringExtra(SearchManager.QUERY)
+ }
+ myPos = baseApplication.lastLocation
+ bindToStatus()
+ binding.appBarMain.main.pullToRefresh.setOnRefreshListener {
+ runUpdateCountriesAndStations()
+ binding.appBarMain.main.pullToRefresh.isRefreshing = false
+ }
+
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ binding.drawerLayout.closeDrawer(GravityCompat.START)
+ } else {
+ finish()
+ }
+ }
+ })
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.main, menu)
+ val manager = getSystemService(SEARCH_SERVICE) as SearchManager
+ val searchMenu = menu.findItem(R.id.search)
+ val search = searchMenu.actionView as SearchView?
+ search!!.setSearchableInfo(manager.getSearchableInfo(componentName))
+ search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(s: String): Boolean {
+ Log.d(TAG, "onQueryTextSubmit: $s")
+ searchString = s
+ updateStationList()
+ return false
+ }
+
+ override fun onQueryTextChange(s: String): Boolean {
+ Log.d(TAG, "onQueryTextChange: $s")
+ searchString = s
+ updateStationList()
+ return false
+ }
+ })
+ val updatePolicy = baseApplication.updatePolicy
+ menu.findItem(updatePolicy.id).isChecked = true
+ return true
+ }
+
+ private fun updateStationList() {
+ try {
+ val sortByDistance = baseApplication.sortByDistance && myPos != null
+ val stationCount = dbAdapter.countStations(baseApplication.countryCodes)
+ val cursor = dbAdapter.getStationsListByKeyword(
+ searchString,
+ baseApplication.stationFilter,
+ baseApplication.countryCodes,
+ sortByDistance,
+ myPos
+ )
+ if (stationListAdapter != null) {
+ stationListAdapter!!.swapCursor(cursor)
+ } else {
+ stationListAdapter = StationListAdapter(this, cursor, 0)
+ binding.appBarMain.main.lstStations.adapter = stationListAdapter
+ binding.appBarMain.main.lstStations.onItemClickListener =
+ OnItemClickListener { _, _, _, id ->
+ val intentDetails = Intent(this@MainActivity, DetailsActivity::class.java)
+ intentDetails.putExtra(
+ DetailsActivity.EXTRA_STATION,
+ dbAdapter.fetchStationByRowId(id)
+ )
+ startActivity(intentDetails)
+ }
+ }
+ binding.appBarMain.main.filterResult.text =
+ getString(R.string.filter_result, stationListAdapter!!.count, stationCount)
+ } catch (e: Exception) {
+ Log.e(TAG, "Unhandled Exception in onQueryTextSubmit", e)
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ val id = item.itemId
+
+ // necessary for the update policy submenu
+ item.isChecked = !item.isChecked
+ when (id) {
+ R.id.rb_update_manual -> {
+ baseApplication.updatePolicy = UpdatePolicy.MANUAL
+ }
+
+ R.id.rb_update_automatic -> {
+ baseApplication.updatePolicy = UpdatePolicy.AUTOMATIC
+ }
+
+ R.id.rb_update_notify -> {
+ baseApplication.updatePolicy = UpdatePolicy.NOTIFY
+ }
+
+ R.id.apiUrl -> {
+ showApiUrlDialog()
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun showApiUrlDialog() {
+ SimpleDialogs.prompt(
+ this,
+ R.string.apiUrl,
+ EditorInfo.TYPE_TEXT_VARIATION_URI,
+ R.string.api_url_hint,
+ baseApplication.apiUrl
+ ) { prompt: String ->
+ baseApplication.apiUrl = prompt
+ baseApplication.lastUpdate = 0
+ recreate()
+ }
+ }
+
+ private fun setNotificationIcon(active: Boolean) {
+ val item = binding.navView.menu.findItem(R.id.nav_notification)
+ item.icon = ContextCompat.getDrawable(
+ this,
+ if (active) R.drawable.ic_notifications_active_gray_24px else R.drawable.ic_notifications_off_gray_24px
+ )
+ }
+
+ override fun onNavigationItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.nav_slideshow -> {
+ startActivity(Intent(this, IntroSliderActivity::class.java))
+ finish()
+ }
+
+ R.id.nav_your_data -> {
+ startActivity(Intent(this, MyDataActivity::class.java))
+ }
+
+ R.id.nav_update_photos -> {
+ runUpdateCountriesAndStations()
+ }
+
+ R.id.nav_notification -> {
+ toggleNotification()
+ }
+
+ R.id.nav_highscore -> {
+ startActivity(Intent(this, HighScoreActivity::class.java))
+ }
+
+ R.id.nav_outbox -> {
+ startActivity(Intent(this, OutboxActivity::class.java))
+ }
+
+ R.id.nav_inbox -> {
+ startActivity(Intent(this, InboxActivity::class.java))
+ }
+
+ R.id.nav_stations_map -> {
+ startActivity(Intent(this, MapsActivity::class.java))
+ }
+
+ R.id.nav_web_site -> {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://railway-stations.org")))
+ }
+
+ R.id.nav_email -> {
+ val emailIntent =
+ Intent(
+ Intent.ACTION_SENDTO,
+ Uri.parse("mailto:" + getString(R.string.fab_email))
+ )
+ emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.fab_subject))
+ startActivity(
+ Intent.createChooser(
+ emailIntent,
+ getString(R.string.fab_chooser_title)
+ )
+ )
+ }
+
+ R.id.nav_app_info -> {
+ AppInfoFragment().show(supportFragmentManager, DIALOG_TAG)
+ }
+ }
+ binding.drawerLayout.closeDrawer(GravityCompat.START)
+ return true
+ }
+
+ private fun runUpdateCountriesAndStations() {
+ binding.appBarMain.main.progressBar.visibility = View.VISIBLE
+ rsapiClient.runUpdateCountriesAndStations(this, baseApplication) { success: Boolean ->
+ if (success) {
+ val tvUpdate = findViewById(R.id.tvUpdate)
+ tvUpdate.text = getString(
+ R.string.last_update_at,
+ SimpleDateFormat.getDateTimeInstance().format(baseApplication.lastUpdate)
+ )
+ updateStationList()
+ }
+ binding.appBarMain.main.progressBar.visibility = View.GONE
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ unregisterLocationManager()
+ }
+
+ public override fun onResume() {
+ super.onResume()
+ for (i in 0 until binding.navView.menu.size()) {
+ binding.navView.menu.getItem(i).isChecked = false
+ }
+ if (baseApplication.lastUpdate == 0L) {
+ runUpdateCountriesAndStations()
+ } else if (System.currentTimeMillis() - baseApplication.lastUpdate > CHECK_UPDATE_INTERVAL) {
+ baseApplication.lastUpdate = System.currentTimeMillis()
+ if (baseApplication.updatePolicy !== UpdatePolicy.MANUAL) {
+ for (country in baseApplication.countryCodes) {
+ rsapiClient.getStatistic(country).enqueue(object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response
+ ) {
+ if (response.isSuccessful) {
+ checkForUpdates(response.body(), country)
+ }
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ Log.e(TAG, "Error loading country statistic", t)
+ }
+ })
+ }
+ }
+ }
+ if (baseApplication.sortByDistance) {
+ registerLocationManager()
+ }
+ binding.appBarMain.main.stationFilterBar.init(baseApplication, this)
+ updateStationList()
+ }
+
+ private fun checkForUpdates(statistic: Statistic?, country: String?) {
+ if (statistic == null) {
+ return
+ }
+ val dbStat = dbAdapter.getStatistic(country)
+ Log.d(TAG, "DbStat: $dbStat")
+ if (statistic.total != dbStat!!.total || statistic.withPhoto != dbStat.withPhoto || statistic.withoutPhoto != dbStat.withoutPhoto) {
+ if (baseApplication.updatePolicy === UpdatePolicy.AUTOMATIC) {
+ runUpdateCountriesAndStations()
+ } else {
+ AlertDialog.Builder(ContextThemeWrapper(this, R.style.AlertDialogCustom))
+ .setIcon(R.mipmap.ic_launcher)
+ .setTitle(R.string.app_name)
+ .setMessage(R.string.update_available)
+ .setCancelable(true)
+ .setPositiveButton(R.string.button_ok_text) { dialog: DialogInterface, _: Int ->
+ runUpdateCountriesAndStations()
+ dialog.dismiss()
+ }
+ .setNegativeButton(R.string.button_cancel_text) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
+ .create().show()
+ }
+ }
+ }
+
+ private fun bindToStatus() {
+ val intent = Intent(this, NearbyNotificationService::class.java)
+ intent.action = NearbyNotificationService.STATUS_INTERFACE
+ if (!this.applicationContext.bindService(intent, object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName, service: IBinder) {
+ Log.d(TAG, "Bound to status service of NearbyNotificationService")
+ statusBinder = service as StatusBinder
+ invalidateOptionsMenu()
+ }
+
+ override fun onServiceDisconnected(name: ComponentName) {
+ Log.d(TAG, "Unbound from status service of NearbyNotificationService")
+ statusBinder = null
+ invalidateOptionsMenu()
+ }
+ }, 0)) {
+ Log.e(TAG, "Bind request to statistics interface failed")
+ }
+ }
+
+ private val requestNotificationPermissionLauncher =
+ registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean? ->
+ if (!isGranted!!) {
+ Toast.makeText(
+ this@MainActivity,
+ R.string.notification_permission_needed,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ private fun toggleNotification() {
+ if (statusBinder == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ requestNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ return
+ }
+ toggleNotificationWithPermissionGranted()
+ }
+
+ private fun toggleNotificationWithPermissionGranted() {
+ val intent = Intent(this@MainActivity, NearbyNotificationService::class.java)
+ if (statusBinder == null) {
+ startService(intent)
+ bindToStatus()
+ setNotificationIcon(true)
+ } else {
+ stopService(intent)
+ setNotificationIcon(false)
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == REQUEST_FINE_LOCATION) {
+ Log.i(TAG, "Received response for location permission request.")
+
+ // Check if the required permission has been granted
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Location permission has been granted
+ registerLocationManager()
+ } else {
+ //Permission not granted
+ baseApplication.sortByDistance = false
+ binding.appBarMain.main.stationFilterBar.setSortOrder(false)
+ }
+ }
+ }
+
+ private fun registerLocationManager() {
+ try {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ && ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ REQUEST_FINE_LOCATION
+ )
+ return
+ }
+ locationManager =
+ applicationContext.getSystemService(LOCATION_SERVICE) as LocationManager
+
+ // getting GPS status
+ val isGPSEnabled = locationManager!!.isProviderEnabled(LocationManager.GPS_PROVIDER)
+
+ // if GPS Enabled get lat/long using GPS Services
+ if (isGPSEnabled) {
+ locationManager!!.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER,
+ MIN_TIME_BW_UPDATES,
+ MIN_DISTANCE_CHANGE_FOR_UPDATES.toFloat(), this
+ )
+ Log.d(TAG, "GPS Enabled")
+ if (locationManager != null) {
+ myPos = locationManager!!.getLastKnownLocation(LocationManager.GPS_PROVIDER)
+ }
+ } else {
+ // getting network status
+ val isNetworkEnabled = locationManager!!
+ .isProviderEnabled(LocationManager.NETWORK_PROVIDER)
+
+ // First get location from Network Provider
+ if (isNetworkEnabled) {
+ locationManager!!.requestLocationUpdates(
+ LocationManager.NETWORK_PROVIDER,
+ MIN_TIME_BW_UPDATES,
+ MIN_DISTANCE_CHANGE_FOR_UPDATES.toFloat(), this
+ )
+ Log.d(TAG, "Network Location enabled")
+ if (locationManager != null) {
+ myPos = locationManager!!
+ .getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error registering LocationManager", e)
+ val b = Bundle()
+ b.putString("error", "Error registering LocationManager: $e")
+ locationManager = null
+ baseApplication.sortByDistance = false
+ binding.appBarMain.main.stationFilterBar.setSortOrder(false)
+ return
+ }
+ Log.i(TAG, "LocationManager registered")
+ onLocationChanged(myPos!!)
+ }
+
+ private fun unregisterLocationManager() {
+ if (locationManager != null) {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ locationManager!!.removeUpdates(this)
+ }
+ locationManager = null
+ }
+ Log.i(TAG, "LocationManager unregistered")
+ }
+
+ override fun onLocationChanged(location: Location) {
+ myPos = location
+ updateStationList()
+ }
+
+ override fun stationFilterChanged(stationFilter: StationFilter) {
+ baseApplication.stationFilter = stationFilter
+ updateStationList()
+ }
+
+ override fun sortOrderChanged(sortByDistance: Boolean) {
+ if (sortByDistance) {
+ registerLocationManager()
+ }
+ updateStationList()
+ }
+
+ companion object {
+ private const val DIALOG_TAG = "App Info Dialog"
+ private const val CHECK_UPDATE_INTERVAL = (10 * 60 * 1000 // 10 minutes
+ ).toLong()
+ private val TAG = MainActivity::class.java.simpleName
+ private const val REQUEST_FINE_LOCATION = 1
+
+ // The minimum distance to change Updates in meters
+ private const val MIN_DISTANCE_CHANGE_FOR_UPDATES: Long = 1000
+
+ // The minimum time between updates in milliseconds
+ private const val MIN_TIME_BW_UPDATES: Long = 500 // minute
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/MapsActivity.kt b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/MapsActivity.kt
new file mode 100644
index 00000000..a7475176
--- /dev/null
+++ b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/MapsActivity.kt
@@ -0,0 +1,1028 @@
+package de.bahnhoefe.deutschlands.bahnhofsfotos
+
+import android.Manifest
+import android.content.DialogInterface
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Color
+import android.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.CombinedVibration
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.os.VibratorManager
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.WindowManager
+import android.widget.CheckBox
+import android.widget.CompoundButton
+import android.widget.Toast
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.documentfile.provider.DocumentFile
+import de.bahnhoefe.deutschlands.bahnhofsfotos.MapsActivity.BahnhofGeoItem
+import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityMapsBinding
+import de.bahnhoefe.deutschlands.bahnhofsfotos.db.DbAdapter
+import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.MapInfoFragment
+import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs
+import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.StationFilterBar
+import de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge.ClusterManager
+import de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge.DbsTileSource
+import de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge.GeoItem
+import de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge.MarkerBitmap
+import de.bahnhoefe.deutschlands.bahnhofsfotos.mapsforge.TapHandler
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Station
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Upload
+import de.bahnhoefe.deutschlands.bahnhofsfotos.util.StationFilter
+import org.mapsforge.core.graphics.Align
+import org.mapsforge.core.graphics.Bitmap
+import org.mapsforge.core.graphics.FontFamily
+import org.mapsforge.core.graphics.FontStyle
+import org.mapsforge.core.graphics.Style
+import org.mapsforge.core.model.LatLong
+import org.mapsforge.core.model.MapPosition
+import org.mapsforge.core.model.Point
+import org.mapsforge.map.android.graphics.AndroidGraphicFactory
+import org.mapsforge.map.android.input.MapZoomControls
+import org.mapsforge.map.android.util.AndroidUtil
+import org.mapsforge.map.datastore.MapDataStore
+import org.mapsforge.map.layer.Layer
+import org.mapsforge.map.layer.cache.TileCache
+import org.mapsforge.map.layer.download.TileDownloadLayer
+import org.mapsforge.map.layer.download.tilesource.AbstractTileSource
+import org.mapsforge.map.layer.download.tilesource.OnlineTileSource
+import org.mapsforge.map.layer.download.tilesource.OpenStreetMapMapnik
+import org.mapsforge.map.layer.overlay.Marker
+import org.mapsforge.map.layer.renderer.TileRendererLayer
+import org.mapsforge.map.model.IMapViewPosition
+import org.mapsforge.map.reader.MapFile
+import org.mapsforge.map.rendertheme.InternalRenderTheme
+import org.mapsforge.map.rendertheme.StreamRenderTheme
+import org.mapsforge.map.rendertheme.XmlRenderTheme
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.lang.ref.WeakReference
+
+class MapsActivity : AppCompatActivity(), LocationListener, TapHandler,
+ StationFilterBar.OnChangeListener {
+ private val onlineTileSources = mutableMapOf()
+ private var layer: Layer? = null
+ private var clusterer: ClusterManager? = null
+ private val tileCaches = mutableListOf()
+ private var myPos: LatLong? = null
+ private var myLocSwitch: CheckBox? = null
+ private lateinit var dbAdapter: DbAdapter
+ private var nickname: String? = null
+ private lateinit var baseApplication: BaseApplication
+ private var locationManager: LocationManager? = null
+ private var askedForPermission = false
+ private var missingMarker: Marker? = null
+ private lateinit var binding: ActivityMapsBinding
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ AndroidGraphicFactory.createInstance(this.application)
+ binding = ActivityMapsBinding.inflate(
+ layoutInflater
+ )
+ setContentView(binding.root)
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ window.statusBarColor = Color.parseColor("#c71c4d")
+ setSupportActionBar(binding.mapsToolbar)
+ supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+ baseApplication = application as BaseApplication
+ dbAdapter = baseApplication.dbAdapter
+ nickname = baseApplication.nickname
+ val intent = intent
+ var extraMarker: Marker? = null
+ if (intent != null) {
+ val latitude = intent.getDoubleExtra(EXTRAS_LATITUDE, 0.0)
+ val longitude = intent.getDoubleExtra(EXTRAS_LATITUDE, 0.0)
+ val markerRes = intent.getIntExtra(EXTRAS_MARKER, -1)
+ setMyLocSwitch(false)
+ if (!(latitude == 0.0 && longitude == 0.0)) {
+ myPos = LatLong(latitude, longitude)
+ }
+ if (markerRes != -1) {
+ extraMarker = createBitmapMarker(myPos!!, markerRes)
+ }
+ }
+ addDBSTileSource(R.string.dbs_osm_basic, "/styles/dbs-osm-basic/")
+ addDBSTileSource(R.string.dbs_osm_railway, "/styles/dbs-osm-railway/")
+ createMapViews()
+ createTileCaches()
+ checkPermissionsAndCreateLayersAndControls()
+ if (extraMarker != null) {
+ binding.map.mapView.layerManager.layers.add(extraMarker)
+ }
+ }
+
+ private fun addDBSTileSource(nameResId: Int, baseUrl: String) {
+ val dbsBasic = DbsTileSource(getString(nameResId), baseUrl)
+ onlineTileSources[dbsBasic.name] = dbsBasic
+ }
+
+ private fun createTileCaches() {
+ tileCaches.add(
+ AndroidUtil.createTileCache(
+ this, this.javaClass.simpleName,
+ binding.map.mapView.model.displayModel.tileSize, 1.0f,
+ binding.map.mapView.model.frameBufferModel.overdrawFactor, true
+ )
+ )
+ }
+
+ /**
+ * Hook to check for Android Runtime Permissions.
+ */
+ private fun checkPermissionsAndCreateLayersAndControls() {
+ createLayers()
+ createControls()
+ }
+
+ /**
+ * Hook to create controls, such as scale bars.
+ * You can add more controls.
+ */
+ private fun createControls() {
+ initializePosition(binding.map.mapView.model.mapViewPosition)
+ }
+
+ /**
+ * initializes the map view position.
+ *
+ * @param mvp the map view position to be set
+ */
+ private fun initializePosition(mvp: IMapViewPosition) {
+ if (myPos != null) {
+ mvp.mapPosition = MapPosition(myPos, baseApplication.zoomLevelDefault)
+ } else {
+ mvp.mapPosition = baseApplication.lastMapPosition
+ }
+ mvp.zoomLevelMax = zoomLevelMax
+ mvp.zoomLevelMin = zoomLevelMin
+ }
+
+ /**
+ * Template method to create the map views.
+ */
+ private fun createMapViews() {
+ binding.map.mapView.isClickable = true
+ binding.map.mapView.setOnMapDragListener { myLocSwitch?.isChecked = false }
+ binding.map.mapView.mapScaleBar.isVisible = true
+ binding.map.mapView.setBuiltInZoomControls(true)
+ binding.map.mapView.mapZoomControls.isAutoHide = true
+ binding.map.mapView.mapZoomControls.zoomLevelMin = zoomLevelMin
+ binding.map.mapView.mapZoomControls.zoomLevelMax = zoomLevelMax
+ binding.map.mapView.mapZoomControls.setZoomControlsOrientation(MapZoomControls.Orientation.VERTICAL_IN_OUT)
+ binding.map.mapView.mapZoomControls.setZoomInResource(R.drawable.zoom_control_in)
+ binding.map.mapView.mapZoomControls.setZoomOutResource(R.drawable.zoom_control_out)
+ binding.map.mapView.mapZoomControls.setMarginHorizontal(
+ resources.getDimensionPixelOffset(
+ R.dimen.controls_margin
+ )
+ )
+ binding.map.mapView.mapZoomControls.setMarginVertical(resources.getDimensionPixelOffset(R.dimen.controls_margin))
+ }
+
+ private val zoomLevelMax: Byte
+ get() = binding.map.mapView.model.mapViewPosition.zoomLevelMax
+ private val zoomLevelMin: Byte
+ get() = binding.map.mapView.model.mapViewPosition.zoomLevelMin
+
+ /**
+ * Hook to purge tile caches.
+ * By default we purge every tile cache that has been added to the tileCaches list.
+ */
+ private fun purgeTileCaches() {
+ for (tileCache in tileCaches) {
+ tileCache.purge()
+ }
+ tileCaches.clear()
+ }
+
+ private val renderTheme: XmlRenderTheme
+ get() {
+ baseApplication.mapThemeUri?.let {
+ try {
+ val renderThemeFile = DocumentFile.fromSingleUri(application, it)
+ return StreamRenderTheme(
+ "/assets/", contentResolver.openInputStream(
+ renderThemeFile!!.uri
+ )
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Error loading theme $it", e)
+ return InternalRenderTheme.DEFAULT
+ }
+ }
+ return InternalRenderTheme.DEFAULT
+ }
+ private val mapFile: MapDataStore?
+ get() {
+ BaseApplication.toUri(baseApplication.map)?.let {
+ if (!DocumentFile.isDocumentUri(this, it)) {
+ return null
+ }
+ try {
+ val inputStream = contentResolver.openInputStream(it) as FileInputStream?
+ return MapFile(inputStream, 0, null)
+ } catch (e: FileNotFoundException) {
+ Log.e(TAG, "Can't open mapFile", e)
+ }
+ null
+ }
+ return null
+ }
+
+ private fun createLayers() {
+ if (mapFile != null) {
+ val rendererLayer: TileRendererLayer = object : TileRendererLayer(
+ tileCaches[0],
+ mapFile,
+ binding.map.mapView.model.mapViewPosition,
+ false,
+ true,
+ false,
+ AndroidGraphicFactory.INSTANCE
+ ) {
+ override fun onLongPress(
+ tapLatLong: LatLong,
+ thisXY: Point?,
+ tapXY: Point?
+ ): Boolean {
+ this@MapsActivity.onLongPress(tapLatLong)
+ return true
+ }
+ }
+ rendererLayer.setXmlRenderTheme(renderTheme)
+ layer = rendererLayer
+ binding.map.mapView.layerManager.layers.add(layer)
+ } else {
+ var tileSource: AbstractTileSource? = onlineTileSources[baseApplication.map]
+ if (tileSource == null) {
+ tileSource = OpenStreetMapMapnik.INSTANCE
+ }
+ tileSource!!.userAgent = USER_AGENT
+ layer = object : TileDownloadLayer(
+ tileCaches[0],
+ binding.map.mapView.model.mapViewPosition, tileSource,
+ AndroidGraphicFactory.INSTANCE
+ ) {
+ override fun onLongPress(
+ tapLatLong: LatLong, thisXY: Point?,
+ tapXY: Point?
+ ): Boolean {
+ this@MapsActivity.onLongPress(tapLatLong)
+ return true
+ }
+ }
+ binding.map.mapView.layerManager.layers.add(layer)
+ binding.map.mapView.setZoomLevelMin(tileSource.zoomLevelMin)
+ binding.map.mapView.setZoomLevelMax(tileSource.zoomLevelMax)
+ }
+ }
+
+ private fun createBitmapMarker(latLong: LatLong, markerRes: Int): Marker {
+ val drawable = ContextCompat.getDrawable(this, markerRes)!!
+ val bitmap = AndroidGraphicFactory.convertToBitmap(drawable)
+ return Marker(latLong, bitmap, -(bitmap.width / 2), -bitmap.height)
+ }
+
+ private fun onLongPress(tapLatLong: LatLong) {
+ if (missingMarker == null) {
+ // marker to show at the location
+ val drawable = ContextCompat.getDrawable(this, R.drawable.marker_missing)!!
+ val bitmap = AndroidGraphicFactory.convertToBitmap(drawable)
+ missingMarker =
+ object : Marker(tapLatLong, bitmap, -(bitmap.width / 2), -bitmap.height) {
+ override fun onTap(tapLatLong: LatLong, layerXY: Point, tapXY: Point): Boolean {
+ SimpleDialogs.confirmOkCancel(
+ this@MapsActivity,
+ R.string.add_missing_station
+ ) { _: DialogInterface?, _: Int ->
+ val intent = Intent(this@MapsActivity, UploadActivity::class.java)
+ intent.putExtra(
+ UploadActivity.EXTRA_LATITUDE,
+ latLong.latitude
+ )
+ intent.putExtra(
+ UploadActivity.EXTRA_LONGITUDE,
+ latLong.longitude
+ )
+ startActivity(intent)
+ }
+ return false
+ }
+ }
+ binding.map.mapView.layerManager.layers.add(missingMarker)
+ } else {
+ missingMarker!!.latLong = tapLatLong
+ missingMarker!!.requestRedraw()
+ }
+
+ vibrationFeedbackForLongClick()
+ }
+
+ private fun vibrationFeedbackForLongClick() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val vibratorManager =
+ getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager
+ val vibrationEffect = VibrationEffect.createOneShot(
+ 150,
+ VibrationEffect.DEFAULT_AMPLITUDE
+ )
+ vibratorManager.vibrate(CombinedVibration.createParallel(vibrationEffect))
+ } else {
+ @Suppress("DEPRECATION")
+ (getSystemService(VIBRATOR_SERVICE) as Vibrator).vibrate(
+ VibrationEffect.createOneShot(
+ 150,
+ VibrationEffect.DEFAULT_AMPLITUDE
+ )
+ )
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ super.onCreateOptionsMenu(menu)
+ menuInflater.inflate(R.menu.maps, menu)
+ val item = menu.findItem(R.id.menu_toggle_mypos)
+ myLocSwitch = CheckBox(this)
+ myLocSwitch?.setButtonDrawable(R.drawable.ic_gps_fix_selector)
+ myLocSwitch?.isChecked = baseApplication.isLocationUpdates
+ item.actionView = myLocSwitch
+ myLocSwitch?.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
+ baseApplication.isLocationUpdates = isChecked
+ if (isChecked) {
+ askedForPermission = false
+ registerLocationManager()
+ } else {
+ unregisterLocationManager()
+ }
+ }
+ val map = baseApplication.map
+ val osmMapnick = menu.findItem(R.id.osm_mapnik)
+ osmMapnick.isChecked = map == null
+ osmMapnick.setOnMenuItemClickListener(MapMenuListener(this, baseApplication, null))
+ val mapSubmenu = menu.findItem(R.id.maps_submenu).subMenu!!
+ for (tileSource in onlineTileSources.values) {
+ val mapItem = mapSubmenu.add(R.id.maps_group, Menu.NONE, Menu.NONE, tileSource.name)
+ mapItem.isChecked = tileSource.name == map
+ mapItem.setOnMenuItemClickListener(
+ MapMenuListener(
+ this,
+ baseApplication,
+ tileSource.name
+ )
+ )
+ }
+ baseApplication.mapDirectoryUri?.let { mapDirectoryUri ->
+ val documentsTree = getDocumentFileFromTreeUri(mapDirectoryUri)
+ if (documentsTree != null) {
+ for (file in documentsTree.listFiles()) {
+ if (file.isFile && file.name!!.endsWith(".map")) {
+ val mapItem =
+ mapSubmenu.add(R.id.maps_group, Menu.NONE, Menu.NONE, file.name)
+ mapItem.isChecked = BaseApplication.toUri(map)?.let {
+ file.uri == it
+ } ?: false
+ mapItem.setOnMenuItemClickListener(
+ MapMenuListener(
+ this,
+ baseApplication,
+ file.uri.toString()
+ )
+ )
+ }
+ }
+ }
+ }
+ mapSubmenu.setGroupCheckable(R.id.maps_group, true, true)
+ val mapFolder = mapSubmenu.add(R.string.map_folder)
+ mapFolder.setOnMenuItemClickListener {
+ openMapDirectoryChooser()
+ false
+ }
+ val mapTheme = baseApplication.mapThemeUri
+ val mapThemeDirectory = baseApplication.mapThemeDirectoryUri
+ val defaultTheme = menu.findItem(R.id.default_theme)
+ defaultTheme.isChecked = mapTheme == null
+ defaultTheme.setOnMenuItemClickListener(MapThemeMenuListener(this, baseApplication, null))
+ val themeSubmenu = menu.findItem(R.id.themes_submenu).subMenu!!
+ mapThemeDirectory?.let {
+ val documentsTree = getDocumentFileFromTreeUri(it)
+ if (documentsTree != null) {
+ for (file in documentsTree.listFiles()) {
+ if (file.isFile && file.name!!.endsWith(".xml")) {
+ val themeName = file.name
+ val themeItem =
+ themeSubmenu.add(R.id.themes_group, Menu.NONE, Menu.NONE, themeName)
+ themeItem.isChecked = mapTheme?.let { uri: Uri -> file.uri == uri }
+ ?: false
+ themeItem.setOnMenuItemClickListener(
+ MapThemeMenuListener(
+ this,
+ baseApplication,
+ file.uri
+ )
+ )
+ } else if (file.isDirectory) {
+ val childFile = file.findFile(file.name + ".xml")
+ if (childFile != null) {
+ val themeName = file.name
+ val themeItem =
+ themeSubmenu.add(R.id.themes_group, Menu.NONE, Menu.NONE, themeName)
+ themeItem.isChecked =
+ mapTheme?.let { uri: Uri -> childFile.uri == uri }
+ ?: false
+ themeItem.setOnMenuItemClickListener(
+ MapThemeMenuListener(
+ this,
+ baseApplication,
+ childFile.uri
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ themeSubmenu.setGroupCheckable(R.id.themes_group, true, true)
+ val themeFolder = themeSubmenu.add(R.string.theme_folder)
+ themeFolder.setOnMenuItemClickListener {
+ openThemeDirectoryChooser()
+ false
+ }
+ return true
+ }
+
+ private fun getDocumentFileFromTreeUri(uri: Uri): DocumentFile? {
+ try {
+ return DocumentFile.fromTreeUri(application, uri)
+ } catch (e: Exception) {
+ Log.w(TAG, "Error getting DocumentFile from Uri: $uri")
+ }
+ return null
+ }
+
+ private var themeDirectoryLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result: ActivityResult ->
+ if (result.resultCode == RESULT_OK && result.data != null) {
+ val uri = result.data!!.data
+ if (uri != null) {
+ contentResolver.takePersistableUriPermission(
+ uri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+ baseApplication.setMapThemeDirectoryUri(uri)
+ recreate()
+ }
+ }
+ }
+
+ private var mapDirectoryLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result: ActivityResult ->
+ if (result.resultCode == RESULT_OK && result.data != null) {
+ val uri = result.data!!.data
+ if (uri != null) {
+ contentResolver.takePersistableUriPermission(
+ uri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+ baseApplication.setMapDirectoryUri(uri)
+ recreate()
+ }
+ }
+ }
+
+ private fun openDirectory(launcher: ActivityResultLauncher) {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
+ launcher.launch(intent)
+ }
+
+ private fun openMapDirectoryChooser() {
+ openDirectory(mapDirectoryLauncher)
+ }
+
+ private fun openThemeDirectoryChooser() {
+ openDirectory(themeDirectoryLauncher)
+ }
+
+ /**
+ * Android Activity life cycle method.
+ */
+ override fun onDestroy() {
+ binding.map.mapView.destroyAll()
+ AndroidGraphicFactory.clearResourceMemoryCache()
+ purgeTileCaches()
+ super.onDestroy()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == R.id.map_info) {
+ MapInfoFragment().show(supportFragmentManager, "Map Info Dialog")
+ } else {
+ return super.onOptionsItemSelected(item)
+ }
+ return true
+ }
+
+ private fun reloadMap() {
+ destroyClusterManager()
+ LoadMapMarkerTask(this).start()
+ }
+
+ private fun runUpdateCountriesAndStations() {
+ binding.map.progressBar.visibility = View.VISIBLE
+ baseApplication.rsapiClient.runUpdateCountriesAndStations(
+ this,
+ baseApplication
+ ) { reloadMap() }
+ }
+
+ private fun onStationsLoaded(stationList: List, uploadList: List) {
+ try {
+ createClusterManager()
+ addMarkers(stationList, uploadList)
+ binding.map.progressBar.visibility = View.GONE
+ Toast.makeText(
+ this,
+ resources.getQuantityString(
+ R.plurals.stations_loaded,
+ stationList.size,
+ stationList.size
+ ),
+ Toast.LENGTH_LONG
+ ).show()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error loading markers", e)
+ }
+ }
+
+ override fun onTap(item: BahnhofGeoItem) {
+ val intent = Intent(this@MapsActivity, DetailsActivity::class.java)
+ val id = item.station.id
+ val country = item.station.country
+ try {
+ val station = dbAdapter.getStationByKey(country, id)
+ intent.putExtra(DetailsActivity.EXTRA_STATION, station)
+ startActivity(intent)
+ } catch (e: RuntimeException) {
+ Log.wtf(
+ TAG,
+ String.format("Could not fetch station id %s that we put onto the map", id),
+ e
+ )
+ }
+ }
+
+ private fun setMyLocSwitch(checked: Boolean) {
+ myLocSwitch?.isChecked = checked
+ baseApplication.isLocationUpdates = checked
+ }
+
+ override fun stationFilterChanged(stationFilter: StationFilter) {
+ reloadMap()
+ }
+
+ override fun sortOrderChanged(sortByDistance: Boolean) {
+ // unused
+ }
+
+ private class LoadMapMarkerTask(activity: MapsActivity) : Thread() {
+ private val activityRef: WeakReference
+
+ init {
+ activityRef = WeakReference(activity)
+ }
+
+ override fun run() {
+ activityRef.get()?.let {
+ val stationList = it.readStations()
+ val uploadList = it.readPendingUploads()
+ it.runOnUiThread { it.onStationsLoaded(stationList, uploadList) }
+ }
+ }
+ }
+
+ private fun readStations(): List {
+ try {
+ return dbAdapter.getAllStations(
+ baseApplication.stationFilter,
+ baseApplication.countryCodes
+ )
+ } catch (e: Exception) {
+ Log.i(TAG, "Datenbank konnte nicht geöffnet werden")
+ }
+ return listOf()
+ }
+
+ private fun readPendingUploads(): List {
+ try {
+ return dbAdapter.getPendingUploads(false)
+ } catch (e: Exception) {
+ Log.i(TAG, "Datenbank konnte nicht geöffnet werden")
+ }
+ return listOf()
+ }
+
+ private fun createMarkerBitmaps(): List {
+ return listOf(
+ createSmallSingleIconMarker(),
+ createSmallClusterIconMarker(),
+ createLargeClusterIconMarker()
+ )
+ }
+
+ /**
+ * large cluster icon. 100 will be ignored.
+ */
+ private fun createLargeClusterIconMarker(): MarkerBitmap {
+ val bitmapBalloonMN = loadBitmap(R.drawable.balloon_m_n)
+ val paint = AndroidGraphicFactory.INSTANCE.createPaint()
+ paint.setStyle(Style.FILL)
+ paint.setTextAlign(Align.CENTER)
+ paint.setTypeface(FontFamily.DEFAULT, FontStyle.BOLD)
+ paint.color = Color.BLACK
+ return MarkerBitmap(
+ this.applicationContext, bitmapBalloonMN,
+ Point(0.0, 0.0), 11f, 100, paint
+ )
+ }
+
+ /**
+ * small cluster icon. for 10 or less items.
+ */
+ private fun createSmallClusterIconMarker(): MarkerBitmap {
+ val bitmapBalloonSN = loadBitmap(R.drawable.balloon_s_n)
+ val paint = AndroidGraphicFactory.INSTANCE.createPaint()
+ paint.setStyle(Style.FILL)
+ paint.setTextAlign(Align.CENTER)
+ paint.setTypeface(FontFamily.DEFAULT, FontStyle.BOLD)
+ paint.color = Color.BLACK
+ return MarkerBitmap(
+ this.applicationContext, bitmapBalloonSN,
+ Point(0.0, 0.0), 9f, 10, paint
+ )
+ }
+
+ private fun createSmallSingleIconMarker(): MarkerBitmap {
+ val bitmapWithPhoto = loadBitmap(R.drawable.marker_green)
+ val markerWithoutPhoto = loadBitmap(R.drawable.marker_red)
+ val markerOwnPhoto = loadBitmap(R.drawable.marker_violet)
+ val markerPendingUpload = loadBitmap(R.drawable.marker_yellow)
+ val markerWithPhotoInactive = loadBitmap(R.drawable.marker_green_inactive)
+ val markerWithoutPhotoInactive = loadBitmap(R.drawable.marker_red_inactive)
+ val markerOwnPhotoInactive = loadBitmap(R.drawable.marker_violet_inactive)
+ val paint = AndroidGraphicFactory.INSTANCE.createPaint()
+ paint.setStyle(Style.FILL)
+ paint.setTextAlign(Align.CENTER)
+ paint.setTypeface(FontFamily.DEFAULT, FontStyle.BOLD)
+ paint.color = Color.RED
+ return MarkerBitmap(
+ this.applicationContext,
+ markerWithoutPhoto,
+ bitmapWithPhoto,
+ markerOwnPhoto,
+ markerWithoutPhotoInactive,
+ markerWithPhotoInactive,
+ markerOwnPhotoInactive,
+ markerPendingUpload,
+ Point(0.0, -(markerWithoutPhoto.height / 2.0)),
+ 10f,
+ 1,
+ paint
+ )
+ }
+
+ private fun loadBitmap(resourceId: Int): Bitmap {
+ val bitmap = AndroidGraphicFactory.convertToBitmap(
+ ResourcesCompat.getDrawable(
+ resources,
+ resourceId,
+ null
+ )
+ )
+ bitmap.incrementRefCount()
+ return bitmap
+ }
+
+ private fun addMarkers(stationMarker: List, uploadList: List) {
+ var minLat = 0.0
+ var maxLat = 0.0
+ var minLon = 0.0
+ var maxLon = 0.0
+ for (station in stationMarker) {
+ val isPendingUpload = isPendingUpload(station, uploadList)
+ val geoItem = BahnhofGeoItem(station, isPendingUpload)
+ val stationPos = geoItem.latLong
+ if (minLat == 0.0) {
+ minLat = stationPos.latitude
+ maxLat = stationPos.latitude
+ minLon = stationPos.longitude
+ maxLon = stationPos.longitude
+ } else {
+ minLat = minLat.coerceAtMost(stationPos.latitude)
+ maxLat = maxLat.coerceAtLeast(stationPos.latitude)
+ minLon = minLon.coerceAtMost(stationPos.longitude)
+ maxLon = maxLon.coerceAtLeast(stationPos.longitude)
+ }
+ clusterer!!.addItem(geoItem)
+ }
+ clusterer!!.redraw()
+ if (myPos == null || myPos!!.latitude == 0.0 && myPos!!.longitude == 0.0) {
+ myPos = LatLong((minLat + maxLat) / 2, (minLon + maxLon) / 2)
+ }
+ updatePosition()
+ }
+
+ private fun isPendingUpload(station: Station?, uploadList: List?): Boolean {
+ for (upload in uploadList!!) {
+ if (upload!!.isPendingPhotoUpload && station!!.id == upload.stationId && station.country == upload.country) {
+ return true
+ }
+ }
+ return false
+ }
+
+ public override fun onResume() {
+ super.onResume()
+ if (layer is TileDownloadLayer) {
+ (layer as TileDownloadLayer?)!!.onResume()
+ }
+ if (baseApplication.lastUpdate == 0L) {
+ runUpdateCountriesAndStations()
+ } else {
+ reloadMap()
+ }
+ if (baseApplication.isLocationUpdates) {
+ registerLocationManager()
+ }
+ binding.map.stationFilterBar.init(baseApplication, this)
+ binding.map.stationFilterBar.setSortOrderEnabled(false)
+ }
+
+ private fun createClusterManager() {
+ // create clusterer instance
+ clusterer = ClusterManager(
+ binding.map.mapView,
+ createMarkerBitmaps(), 9.toByte(),
+ this
+ )
+ // this uses the framebuffer position, the mapview position can be out of sync with
+ // what the user sees on the screen if an animation is in progress
+ binding.map.mapView.model.frameBufferModel.addObserver(clusterer)
+ }
+
+ override fun onPause() {
+ if (layer is TileDownloadLayer) {
+ (layer as TileDownloadLayer?)!!.onPause()
+ }
+ unregisterLocationManager()
+ val mapPosition = binding.map.mapView.model.mapViewPosition.mapPosition
+ baseApplication.lastMapPosition = mapPosition
+ destroyClusterManager()
+ super.onPause()
+ }
+
+ private fun destroyClusterManager() {
+ if (clusterer != null) {
+ clusterer!!.destroyGeoClusterer()
+ binding.map.mapView.model.frameBufferModel.removeObserver(clusterer)
+ clusterer = null
+ }
+ }
+
+ override fun onLocationChanged(location: Location) {
+ myPos = LatLong(location.latitude, location.longitude)
+ updatePosition()
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {
+ }
+
+ override fun onProviderEnabled(provider: String) {}
+ override fun onProviderDisabled(provider: String) {}
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == REQUEST_FINE_LOCATION) {
+ Log.i(TAG, "Received response for location permission request.")
+
+ // Check if the required permission has been granted
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Location permission has been granted
+ registerLocationManager()
+ } else {
+ //Permission not granted
+ Toast.makeText(
+ this@MapsActivity,
+ R.string.grant_location_permission,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+
+ private fun registerLocationManager() {
+ try {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ if (!askedForPermission) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ REQUEST_FINE_LOCATION
+ )
+ askedForPermission = true
+ }
+ setMyLocSwitch(false)
+ return
+ }
+ locationManager =
+ applicationContext.getSystemService(LOCATION_SERVICE) as LocationManager
+
+ // getting GPS status
+ val isGPSEnabled = locationManager!!
+ .isProviderEnabled(LocationManager.GPS_PROVIDER)
+
+ // if GPS Enabled get lat/long using GPS Services
+ if (isGPSEnabled) {
+ locationManager!!.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER,
+ MIN_TIME_BW_UPDATES,
+ MIN_DISTANCE_CHANGE_FOR_UPDATES.toFloat(), this
+ )
+ Log.d(TAG, "GPS Enabled")
+ if (locationManager != null) {
+ val loc = locationManager!!
+ .getLastKnownLocation(LocationManager.GPS_PROVIDER)
+ myPos = LatLong(loc!!.latitude, loc.longitude)
+ }
+ } else {
+ // getting network status
+ val isNetworkEnabled = locationManager!!
+ .isProviderEnabled(LocationManager.NETWORK_PROVIDER)
+
+ // First get location from Network Provider
+ if (isNetworkEnabled) {
+ locationManager!!.requestLocationUpdates(
+ LocationManager.NETWORK_PROVIDER,
+ MIN_TIME_BW_UPDATES,
+ MIN_DISTANCE_CHANGE_FOR_UPDATES.toFloat(), this
+ )
+ Log.d(TAG, "Network Location enabled")
+ if (locationManager != null) {
+ val loc = locationManager!!
+ .getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
+ myPos = LatLong(loc!!.latitude, loc.longitude)
+ }
+ }
+ }
+ setMyLocSwitch(true)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error registering LocationManager", e)
+ val b = Bundle()
+ b.putString("error", "Error registering LocationManager: $e")
+ locationManager = null
+ myPos = null
+ setMyLocSwitch(false)
+ return
+ }
+ Log.i(TAG, "LocationManager registered")
+ updatePosition()
+ }
+
+ private fun unregisterLocationManager() {
+ if (locationManager != null) {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ locationManager!!.removeUpdates(this)
+ }
+ locationManager = null
+ }
+ Log.i(TAG, "LocationManager unregistered")
+ }
+
+ private fun updatePosition() {
+ if (myLocSwitch?.isChecked == true) {
+ binding.map.mapView.setCenter(myPos)
+ binding.map.mapView.repaint()
+ }
+ }
+
+ inner class BahnhofGeoItem(
+ var station: Station,
+ override val isPendingUpload: Boolean
+ ) : GeoItem {
+ override val latLong: LatLong = LatLong(station.lat, station.lon)
+
+ override val title: String
+ get() = station.title
+
+ override fun hasPhoto(): Boolean {
+ return station.hasPhoto()
+ }
+
+ override fun ownPhoto(): Boolean {
+ return hasPhoto() && station.photographer == nickname
+ }
+
+ override fun stationActive(): Boolean {
+ return station.active
+ }
+ }
+
+ private class MapMenuListener(
+ mapsActivity: MapsActivity,
+ private val baseApplication: BaseApplication,
+ private val map: String?
+ ) : MenuItem.OnMenuItemClickListener {
+ private val mapsActivityRef: WeakReference
+
+ init {
+ mapsActivityRef = WeakReference(mapsActivity)
+ }
+
+ override fun onMenuItemClick(item: MenuItem): Boolean {
+ item.isChecked = true
+ if (item.itemId == R.id.osm_mapnik) { // default Mapnik online tiles
+ baseApplication.map = null
+ } else {
+ baseApplication.map = map
+ }
+ val mapsActivity = mapsActivityRef.get()
+ mapsActivity?.recreate()
+ return false
+ }
+ }
+
+ private class MapThemeMenuListener(
+ mapsActivity: MapsActivity,
+ private val baseApplication: BaseApplication?,
+ private val mapThemeUri: Uri?
+ ) : MenuItem.OnMenuItemClickListener {
+ private val mapsActivityRef: WeakReference
+
+ init {
+ mapsActivityRef = WeakReference(mapsActivity)
+ }
+
+ override fun onMenuItemClick(item: MenuItem): Boolean {
+ item.isChecked = true
+ if (item.itemId == R.id.default_theme) { // default theme
+ baseApplication!!.setMapThemeUri(null)
+ } else {
+ baseApplication!!.setMapThemeUri(mapThemeUri)
+ }
+ val mapsActivity = mapsActivityRef.get()
+ mapsActivity?.recreate()
+ return false
+ }
+ }
+
+ companion object {
+ const val EXTRAS_LATITUDE = "Extras_Latitude"
+ const val EXTRAS_LONGITUDE = "Extras_Longitude"
+ const val EXTRAS_MARKER = "Extras_Marker"
+
+ // The minimum distance to change Updates in meters
+ private const val MIN_DISTANCE_CHANGE_FOR_UPDATES: Long = 1 // meters
+
+ // The minimum time between updates in milliseconds
+ private const val MIN_TIME_BW_UPDATES: Long = 500 // minute
+ private val TAG = MapsActivity::class.java.simpleName
+ private const val REQUEST_FINE_LOCATION = 1
+ private const val USER_AGENT = "railway-stations.org-android"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/MyDataActivity.kt b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/MyDataActivity.kt
new file mode 100644
index 00000000..ccf94396
--- /dev/null
+++ b/app/src/main/kotlin/de/bahnhoefe/deutschlands/bahnhofsfotos/MyDataActivity.kt
@@ -0,0 +1,490 @@
+package de.bahnhoefe.deutschlands.bahnhofsfotos
+
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.util.Patterns
+import android.view.ContextThemeWrapper
+import android.view.View
+import android.widget.EditText
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ActivityMydataBinding
+import de.bahnhoefe.deutschlands.bahnhofsfotos.databinding.ChangePasswordBinding
+import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs
+import de.bahnhoefe.deutschlands.bahnhofsfotos.dialogs.SimpleDialogs.confirmOk
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.License
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Profile
+import de.bahnhoefe.deutschlands.bahnhofsfotos.model.Token
+import de.bahnhoefe.deutschlands.bahnhofsfotos.rsapi.RSAPIClient
+import org.apache.commons.lang3.StringUtils
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import java.io.UnsupportedEncodingException
+import java.net.MalformedURLException
+import java.net.URL
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
+import java.security.NoSuchAlgorithmException
+
+class MyDataActivity : AppCompatActivity() {
+ private var license: License? = null
+ private lateinit var baseApplication: BaseApplication
+ private lateinit var rsapiClient: RSAPIClient
+ private lateinit var profile: Profile
+ private lateinit var binding: ActivityMydataBinding
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMydataBinding.inflate(
+ layoutInflater
+ )
+ setContentView(binding.root)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar!!.setTitle(R.string.login)
+ binding.myData.profileForm.visibility = View.INVISIBLE
+ baseApplication = application as BaseApplication
+ rsapiClient = baseApplication.rsapiClient
+ setProfileToUI(baseApplication.profile)
+ oauthAuthorizationCallback(intent)
+ if (isLoginDataAvailable) {
+ loadRemoteProfile()
+ }
+ binding.myData.btLogin.setOnClickListener { login() }
+ binding.myData.tvEmailVerification.setOnClickListener { requestEmailVerification() }
+ binding.myData.cbLicenseCC0.setOnClickListener { selectLicense() }
+ binding.myData.cbAnonymous.setOnClickListener { onAnonymousChecked() }
+ binding.myData.btProfileSave.setOnClickListener { save() }
+ binding.myData.btLogout.setOnClickListener { logout() }
+ binding.myData.btChangePassword.setOnClickListener { changePassword() }
+ binding.myData.btDeleteAccount.setOnClickListener { deleteAccount() }
+
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ startActivity(Intent(this@MyDataActivity, MainActivity::class.java))
+ finish()
+ }
+ })
+ }
+
+ private fun setProfileToUI(profile: Profile) {
+ binding.myData.etNickname.setText(profile.nickname)
+ binding.myData.etEmail.setText(profile.email)
+ binding.myData.etLinking.setText(profile.link)
+ license = profile.license
+ binding.myData.cbLicenseCC0.isChecked = license === License.CC0
+ binding.myData.cbOwnPhoto.isChecked = profile.photoOwner
+ binding.myData.cbAnonymous.isChecked = profile.anonymous
+ onAnonymousChecked()
+ if (profile.emailVerified) {
+ binding.myData.tvEmailVerification.setText(R.string.emailVerified)
+ binding.myData.tvEmailVerification.setTextColor(
+ resources.getColor(
+ R.color.emailVerified,
+ null
+ )
+ )
+ } else {
+ binding.myData.tvEmailVerification.setText(R.string.emailUnverified)
+ binding.myData.tvEmailVerification.setTextColor(
+ resources.getColor(
+ R.color.emailUnverified,
+ null
+ )
+ )
+ }
+ this.profile = profile
+ }
+
+ private fun loadRemoteProfile() {
+ binding.myData.loginForm.visibility = View.VISIBLE
+ binding.myData.profileForm.visibility = View.GONE
+ binding.myData.progressBar.visibility = View.VISIBLE
+ rsapiClient.getProfile().enqueue(object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ binding.myData.progressBar.visibility = View.GONE
+ when (response.code()) {
+ 200 -> {
+ Log.i(TAG, "Successfully loaded profile")
+ val remoteProfile = response.body()
+ if (remoteProfile != null) {
+ saveLocalProfile(remoteProfile)
+ showProfileView()
+ }
+ }
+
+ 401 -> {
+ logout()
+ confirmOk(this@MyDataActivity, R.string.authorization_failed)
+ }
+
+ else -> confirmOk(
+ this@MyDataActivity,
+ getString(R.string.read_profile_failed, response.code().toString())
+ )
+ }
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ binding.myData.progressBar.visibility = View.GONE
+ confirmOk(
+ this@MyDataActivity,
+ getString(R.string.read_profile_failed, t.message)
+ )
+ }
+ })
+ }
+
+ private fun showProfileView() {
+ binding.myData.loginForm.visibility = View.GONE
+ binding.myData.profileForm.visibility = View.VISIBLE
+ supportActionBar?.setTitle(R.string.tvProfile)
+ binding.myData.btProfileSave.setText(R.string.bt_mydata_commit)
+ binding.myData.btLogout.visibility = View.VISIBLE
+ binding.myData.btChangePassword.visibility = View.VISIBLE
+ }
+
+ private fun oauthAuthorizationCallback(intent: Intent?) {
+ if (intent != null && Intent.ACTION_VIEW == intent.action) {
+ val data = intent.data
+ if (data != null) {
+ if (data.toString().startsWith(rsapiClient.redirectUri)) {
+ val code = data.getQueryParameter("code")
+ if (code != null) {
+ rsapiClient.requestAccessToken(code)
+ .enqueue(object : Callback {
+ override fun onResponse(
+ call: Call