public class App extends Application {

    private static AppComponent appComponent;

    public static AppComponent components() {
        return appComponent;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        if (BuildConfig.DEBUG) {
            Timber.plant(new Timber.DebugTree());
        } else {
            Timber.plant(new NoOpTree());
        }

        appComponent = DaggerAppComponent.builder().app(this).build();
    }

    private static class NoOpTree extends Timber.Tree {
        @Override
        protected void log(int priority, String tag, @NotNull String message, Throwable t) {
        }
    }
} public final class NetworkConnectivityChangedEvent {
    public boolean isConnected;
} public class ShowGpxHandler {

    private final SharedPreferences sharedPreferences;
    private final GpxModel gpxModel;
    private final App app;
    private final GpxReader gpxReader;


    @Inject
    public ShowGpxHandler(SharedPreferences sharedPreferences, GpxModel gpxModel, App app, GpxReader gpxReader) {
        this.sharedPreferences = sharedPreferences;
        this.gpxModel = gpxModel;
        this.app = app;
        this.gpxReader = gpxReader;
    }

    public void showGpx(MapView mapView) {
        boolean showTrack = new BooleanPreference(sharedPreferences, SharedPrefsKeys.SHOW_GPX).get();
        if (!showTrack) {
            return;
        }

        String gpxUri = new StringPreference(sharedPreferences, SharedPrefsKeys.GPX_FILE).get();
        if (gpxModel.getUri() == null || !gpxModel.getUri().equals(gpxUri)) {
            readFile(gpxUri);
        }

        showModelOnMap(mapView);
    }

    private void readFile(String gpxUri) {
        try {
            InputStream gpxInputStream = app.getContentResolver().openInputStream(Uri.parse(gpxUri));
            gpxReader.readDataFromStream(gpxInputStream, gpxUri);
        } catch (SecurityException | IOException | SAXException | ParserConfigurationException e) {
            Toast.makeText(app, R.string.gpx_reading_error, Toast.LENGTH_SHORT).show();
        }
    }

    private void showModelOnMap(MapView mapView) {
        for (GpxTrack track : gpxModel.getTracks()) {
            addTrackToMap(mapView, track);
        }

        for (GpxPoi poi : gpxModel.getPoiList()) {
            addPoiToMap(mapView, poi);
        }
    }

    private void addTrackToMap(MapView mapView, GpxTrack track) {
        Polyline trackLine = new Polyline(mapView);
        trackLine.setPoints(track.getWaypoints());
        trackLine.setTitle(track.getName());
        trackLine.getOutlinePaint().setColor(Color.RED);
        mapView.getOverlayManager().add(trackLine);
    }

    private void addPoiToMap(MapView mapView, GpxPoi poi) {
        Marker marker = new Marker(mapView);
        marker.setPosition(poi.getPosition());
        marker.setTitle(poi.getName());
        mapView.getOverlayManager().add(marker);
    }
} @Singleton
public class LocationUpdateManager {

    private final OwnLocationModel ownLocationModel;
    private final EventBus eventBus;
    private final PermissionCheckHandler permissionCheckHandler;
    private final App app;
    private boolean isUpdating = false;

    //const
    private static final float LOCATION_REFRESH_DISTANCE = 20; //20 meters
    private static final long LOCATION_REFRESH_TIME = 12 * 1000; //12 seconds
    private static final int LOCATION_NEW_THRESHOLD = 30 * 1000; //30 seconds
    private static final String[] USED_PROVIDERS = new String[]{
            LocationManager.GPS_PROVIDER,
            LocationManager.NETWORK_PROVIDER};

    //misc
    private final LocationManager locationManager;
    private Location lastPublishedLocation;

    private final LocationListener locationListener = new LocationListener() {
        @Override
        public void onLocationChanged(final Location location) {
            if (shouldPublishNewLocation(location)) {
                publishNewLocation(location);
                lastPublishedLocation = location;
            }
        }

        @Override
        public void onStatusChanged(String s, int i, Bundle bundle) {
            // Notes after some testing:
            // - seems to be only called for GPS provider
            // - calls not necessarily consistent with location fixes
            // - recurrent AVAILABLE calls for GPS
            // -> not usable
        }

        @Override
        public void onProviderEnabled(String s) {
            postStatusEvent();
        }

        @Override
        public void onProviderDisabled(String s) {
            postStatusEvent();
        }
    };

    @Inject
    public LocationUpdateManager(App app,
                                 OwnLocationModel ownLocationModel,
                                 EventBus eventBus,
                                 PermissionCheckHandler permissionCheckHandler) {
        this.app = app;
        this.ownLocationModel = ownLocationModel;
        this.eventBus = eventBus;
        this.permissionCheckHandler = permissionCheckHandler;
        locationManager = (LocationManager) app.getSystemService(Context.LOCATION_SERVICE);
    }

    @Produce
    public GpsStatusChangedEvent produceStatusEvent() {
        return Events.GPS_STATUS_CHANGED_EVENT;
    }

    @Produce
    public NewLocationEvent produceLocationEvent() {
        return Events.NEW_LOCATION_EVENT;
    }

    private void postStatusEvent() {
        setStatusEvent();
        eventBus.post(Events.GPS_STATUS_CHANGED_EVENT);
    }

    private void setAndPostPermissionPermanentlyDeniedEvent() {
        Events.GPS_STATUS_CHANGED_EVENT.status =
                GpsStatusChangedEvent.Status.PERMISSION_PERMANENTLY_DENIED;
        eventBus.post(Events.GPS_STATUS_CHANGED_EVENT);
    }

    private void setStatusEvent() {
        // isProviderEnabled() doesn't throw when permission is not granted, so we can use it safely
        if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
            Events.GPS_STATUS_CHANGED_EVENT.status = GpsStatusChangedEvent.Status.HIGH_ACCURACY;
            isUpdating = true;
        } else if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
            Events.GPS_STATUS_CHANGED_EVENT.status = GpsStatusChangedEvent.Status.LOW_ACCURACY;
            isUpdating = true;
        } else {
            isUpdating = false;
            Events.GPS_STATUS_CHANGED_EVENT.status = GpsStatusChangedEvent.Status.DISABLED;
        }
    }

    private boolean checkIfAtLeastOneProviderExits() {
        final List<String> allProviders = locationManager.getAllProviders();
        boolean atLeastOneProviderExists = false;
        for (String provider : USED_PROVIDERS) {
            if (allProviders.contains(provider)) {
                atLeastOneProviderExists = true;
            }
        }
        return atLeastOneProviderExists;
    }

    // Usage of this method should rather be handled by listening to the GpsStatusChangedEvent,
    // unfortunately this is not possible in PullServerHandler because we can't register on non
    // main thread
    public boolean isUpdating() {
        return isUpdating;
    }

    public void initializeAndStartListening() {
        boolean noProviderExists = !checkIfAtLeastOneProviderExits();
        boolean noPermission = !checkPermission();

        if (noProviderExists) {
            Events.GPS_STATUS_CHANGED_EVENT.status = GpsStatusChangedEvent.Status.NONEXISTENT;
        } else if (noPermission) {
            Events.GPS_STATUS_CHANGED_EVENT.status = GpsStatusChangedEvent.Status.NO_PERMISSIONS;
        } else {
            setStatusEvent();
        }
        eventBus.register(this);

        // Short-circuit here: if no provider exists don't start listening
        if (noProviderExists) {
            return;
        }

        // If permissions are not granted, don't start listening
        if (noPermission) {
            return;
        }

        startListening();
    }

    public void startListening() {
        // Set GPS status in case we're coming back after permission request
        postStatusEvent();

        // To get a quick first location, query all providers for last known location and treat them
        // like regular fixes by piping them through our normal flow
        final List<String> providers = locationManager.getAllProviders();
        for (String provider : providers) {
            @SuppressLint("MissingPermission")
            Location location = locationManager.getLastKnownLocation(provider);
            if (location != null) {
                locationListener.onLocationChanged(location);
            }
        }

        registerLocationListeners();
    }

    private void registerLocationListeners() {
        // register existing providers; if one isn't enabled, the listener will take care of that
        requestLocationUpdatesIfProviderExists(LocationManager.GPS_PROVIDER);
        requestLocationUpdatesIfProviderExists(LocationManager.NETWORK_PROVIDER);
    }

    @SuppressLint("MissingPermission")
    private void requestLocationUpdatesIfProviderExists(String provider) {
        if (locationManager.getProvider(provider) != null) {
            locationManager.requestLocationUpdates(provider,
                    LOCATION_REFRESH_TIME,
                    LOCATION_REFRESH_DISTANCE,
                    locationListener);
        }
    }

    public boolean checkPermission() {
        return PermissionCheckHandler.checkPermissionGranted(
                Manifest.permission.ACCESS_FINE_LOCATION); //TODO does this exist on devices with only network location?
    }

    public void requestPermission() {
        PermissionRequest permissionRequest = new PermissionRequest(
                Manifest.permission.ACCESS_FINE_LOCATION,
                app.getString(R.string.map_location_permissions_rationale_text),
                this::startListening,
                null,
                this::setAndPostPermissionPermanentlyDeniedEvent);
        permissionCheckHandler.requestPermissionWithRationaleIfNeeded(permissionRequest);
    }

    public void handleShutdown() {
        locationManager.removeUpdates(locationListener);
        try {
            eventBus.unregister(this);
        } catch (IllegalArgumentException ignored) {
        }
    }

    private void publishNewLocation(Location location) {
        GeoPoint newLocation = new GeoPoint(location.getLatitude(), location.getLongitude());
        ownLocationModel.setLocation(newLocation, location.getAccuracy());
        eventBus.post(Events.NEW_LOCATION_EVENT);
    }

    private boolean shouldPublishNewLocation(Location location) {
        // Any location is better than no location
        if (lastPublishedLocation == null) {
            return true;
        }

        // Average speed of the CM is ~4 m/s so anything over 30 seconds old, may already
        // be well over 120m off. So a newer fix is assumed to be always better.
        long timeDelta = location.getTime() - lastPublishedLocation.getTime();
        boolean isSignificantlyNewer = timeDelta > LOCATION_NEW_THRESHOLD;
        boolean isSignificantlyOlder = timeDelta < -LOCATION_NEW_THRESHOLD;
        boolean isNewer = timeDelta > 0;

        if (isSignificantlyNewer) {
            return true;
        } else if (isSignificantlyOlder) {
            return false;
        }

        int accuracyDelta = (int) (location.getAccuracy() - lastPublishedLocation.getAccuracy());
        boolean isLessAccurate = accuracyDelta > 0;
        boolean isMoreAccurate = accuracyDelta < 0;
        boolean isSignificantlyLessAccurate = accuracyDelta > 120;

        boolean isFromSameProvider = location.getProvider().equals(lastPublishedLocation.getProvider());

        if (isMoreAccurate) {
            return true;
        } else if (isNewer && !isLessAccurate) {
            return true;
        } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
            return true;
        }

        return false;
    }
} @Singleton
public class OtherUsersLocationModel {

    private ArrayList<GeoPoint> otherUsersLocations = new ArrayList<>();

    @Inject
    public OtherUsersLocationModel() {
    }

    public void setFromJson(JSONObject jsonObject) throws JSONException {
        otherUsersLocations = new ArrayList<>(jsonObject.length());

        Iterator<String> keys = jsonObject.keys();
        while (keys.hasNext()) {
            String key = keys.next();
            JSONObject value = jsonObject.getJSONObject(key);
            int latitudeE6 = Integer.parseInt(value.getString("latitude"));
            int longitudeE6 = Integer.parseInt(value.getString("longitude"));

            otherUsersLocations.add(
                    new GeoPoint(latitudeE6 / 1000000.0D, longitudeE6 / 1000000.0D));
        }
    }

    public ArrayList<GeoPoint> getOtherUsersLocations() {
        return otherUsersLocations;
    }
} @Singleton
public class OwnLocationModel {

    private static final float ACCURACY_PRECISE_THRESHOLD = 50.0f; //meters

    public GeoPoint ownLocation;
    private boolean isLocationPrecise;

    @Inject
    public OwnLocationModel() {
    }

    public void setLocation(@NonNull GeoPoint location, float accuracy) {
        ownLocation = location;
        isLocationPrecise = accuracy < ACCURACY_PRECISE_THRESHOLD;
    }

    public boolean hasPreciseLocation() {
        return ownLocation != null && isLocationPrecise;
    }

    @NonNull
    public JSONObject getLocationJson() {
        JSONObject locationObject = new JSONObject();
        try {
            locationObject.put("longitude", Integer.toString((int) (ownLocation.getLongitude() * 1000000.0D)));
            locationObject.put("latitude", Integer.toString((int) (ownLocation.getLatitude() * 1000000.0D)));
        } catch (JSONException e) {
            Timber.e(e);
        }
        return locationObject;
    }
} public interface SharedPrefsKeys {
    String PRIVACY_POLICY_ACCEPTED =
            BuildConfig.APPLICATION_ID + ".PRIVACY_POLICY_ACCEPTED";
    String OSMDROID_BASE_PATH =
            BuildConfig.APPLICATION_ID + ".OSMDROID_BASE_PATH";
    String OBSERVER_MODE_ACTIVE =
            BuildConfig.APPLICATION_ID + ".OBSERVER_MODE_ACTIVE";
    String SHOW_ON_LOCKSCREEN =
            BuildConfig.APPLICATION_ID + ".SHOW_ON_LOCKSCREEN";
    String KEEP_SCREEN_ON =
            BuildConfig.APPLICATION_ID + ".KEEP_SCREEN_ON";
    String DISABLE_MAP_ROTATION =
            BuildConfig.APPLICATION_ID + ".DISABLE_MAP_ROTATION";
    String USE_HIGH_RES_MAP_TILES =
            BuildConfig.APPLICATION_ID + ".USE_HIGH_RES_MAP_TILES";
    String SHOW_GPX =
            BuildConfig.APPLICATION_ID + ".SHOW_GPX";
    String GPX_FILE =
            BuildConfig.APPLICATION_ID + ".GPX_FILE";
} @Singleton
public class EventBus {

    private final Bus bus = new Bus();

    @Inject
    public EventBus() {
    }

    public void post(Object event) {
        bus.post(event);
    }

    public void register(Object object) {
        bus.register(object);
    }

    public void unregister(Object object) {
        bus.unregister(object);
    }
} public class AlertBuilder {
    public static void show(Activity activity, @StringRes int title, @StringRes int text) {
        new AlertDialog.Builder(activity, R.style.AlertDialogTheme)
                .setTitle(activity.getString(title))
                .setMessage(activity.getString(text))
                .setCancelable(false)
                .setPositiveButton(activity.getString(R.string.ok), null)
                .create()
                .show();
    }
} public class MapViewUtils {
    /*
    private static final String TILE_SOURCE_BASEURL = "https://maps.wikimedia.org/osm-intl/";
    private static final String TILE_SOURCE_COPYRIGHT =
            "Wikimedia maps | Map data © OpenStreetMap contributors";
    */

    private MapViewUtils() {
    }

    public static MapView createMapView(Activity activity) {
        IConfigurationProvider configuration = Configuration.getInstance();

        StorageLocationProvider.StorageLocation storageLocation =
                App.components().storageProvider().getActiveStorageLocation();
        boolean noStoredTilesExist = storageLocation == null;
        if (noStoredTilesExist) {
            storageLocation = App.components().storageProvider().getAndSaveBestStorageLocation();
        }
        File osmdroidBasePath = storageLocation.osmdroidBasePath;
        File osmdroidTileCache = storageLocation.osmdroidTilePath;

        Timber.d("Setting osmdroidBasePath to: %s", osmdroidBasePath.getAbsolutePath());
        configuration.setOsmdroidBasePath(osmdroidBasePath);
        Timber.d("Setting osmdroidTileCache to: %s", osmdroidTileCache.getAbsolutePath());
        configuration.setOsmdroidTileCache(osmdroidTileCache);

        setMaxCacheSize(configuration);

        // TODO Add option to adjust expiration?
        //      setExpirationExtendedDuration() OR setExpirationOverrideDuration()

        configuration.setMapViewHardwareAccelerated(true);
        configuration.setUserAgentValue(BuildConfig.APPLICATION_ID + "/"
                + BuildConfig.VERSION_NAME + " " + org.osmdroid.library.BuildConfig.APPLICATION_ID
                + "/" + org.osmdroid.library.BuildConfig.VERSION_NAME
                + " (" + activity.getString(R.string.contact_email) + ")");

        /*
        BooleanPreference useHighResTilesPreference = new BooleanPreference(
                App.components().sharedPreferences(), SharedPrefsKeys.USE_HIGH_RES_MAP_TILES);

        // Default to high res on clean run of the app (either first run or after app cache clear)
        // If there already cached tiles, don't assume user wants to switch to high res
        if (!useHighResTilesPreference.isSet()) {
            useHighResTilesPreference.set(noStoredTilesExist);
        }

        boolean useHighResTiles = useHighResTilesPreference.get();

        OnlineTileSourceBase onlineTileSourceBase = determineTileResolution(useHighResTiles);
        Timber.d("Using %s tilesource.", onlineTileSourceBase.toString());
         */

        OnlineTileSourceBase onlineTileSourceBase = TileSourceFactory.MAPNIK;

        MapTileProviderBasic mapTileProviderBasic =
                new MapTileProviderBasic(activity.getApplicationContext(), onlineTileSourceBase);

        MapView mapView = new MapView(activity, mapTileProviderBasic);
        mapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);

        mapView.setMultiTouchControls(true);
        mapView.getController().setZoom(1.0d);
        mapView.getController().setCenter(new GeoPoint(0.0d, 0.0d));
        mapView.setClickable(true);
        mapView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT));
        mapView.setTilesScaledToDpi(true);
        mapView.getOverlayManager()
                .getTilesOverlay()
                .setLoadingBackgroundColor(
                        ContextCompat.getColor(activity, R.color.map_loading_tile_color));
        mapView.getOverlayManager()
                .getTilesOverlay()
                .setLoadingLineColor(
                        ContextCompat.getColor(activity, R.color.map_loading_line_color));

        return mapView;
    }

    public static InfoWindow createObserverInfoWindow(MapView mapView) {
        return new InfoWindow(R.layout.view_observer_infowindow, mapView) {
            @Override
            public void onOpen(Object item) {
                mView.setOnClickListener(v -> close());
            }

            @Override
            public void onClose() {

            }
        };
    }

    /*
    private static OnlineTileSourceBase determineTileResolution(boolean useHighResTiles) {
        // When in doubt use the next higher quality tiles.
        // Still scale with mapView.setTilesScaledToDpi(true). Usually this will be 1:1 but when
        // not will ensure consistent appearance across devices.
        // If high res option is off, slip density determination and always use 1x.
        float density = 0f;
        if (useHighResTiles) {
            density = App.components().app().getResources().getDisplayMetrics().density;
        }

        if (density <= 1) {
            return getWikimediaTileSource("Wikimedia", 256, ".png");
        } else if (density <= 1.3) {
            return getWikimediaTileSource("Wikimedia_1_3x", 332, "@1.3x.png");
        } else if (density <= 1.5) {
            return getWikimediaTileSource("Wikimedia_1_5x", 384, "@1.5x.png");
        } else if (density <= 2.0) {
            return getWikimediaTileSource("Wikimedia_2x", 512, "@2x.png");
        } else if (density <= 2.6) {
            return getWikimediaTileSource("Wikimedia_2_6x", 665, "@2.6x.png");
        } else {
            return getWikimediaTileSource("Wikimedia_3x", 768, "@3x.png");
        }
    }

    @NotNull
    private static XYTileSource getWikimediaTileSource(
            String name, int tileSize, String filenameEnding) {
        return new XYTileSource(
                name, 1, 19, tileSize,
                filenameEnding, new String[]{TILE_SOURCE_BASEURL}, TILE_SOURCE_COPYRIGHT);
    }
    */

    private static void setMaxCacheSize(IConfigurationProvider configuration) {
        // code adapted from osmdroid's DefaultConfigurationProvider.load()
        long cacheSize = 0;
        File dbFile = new File(configuration.getOsmdroidTileCache().getAbsolutePath()
                + File.separator + SqlTileWriter.DATABASE_FILENAME);
        if (dbFile.exists()) {
            cacheSize = dbFile.length();
        }

        long freeSpace = configuration.getOsmdroidTileCache().getFreeSpace();
        Timber.d("cacheSize: %d", cacheSize);
        Timber.d("freeSpace: %d", freeSpace);
        Timber.d("getTileFileSystemCacheMaxBytes(): %d",
                configuration.getTileFileSystemCacheMaxBytes());

        if (configuration.getTileFileSystemCacheMaxBytes() > (freeSpace + cacheSize)) {
            configuration.setTileFileSystemCacheMaxBytes((long) ((freeSpace + cacheSize) * 0.95));
            configuration.setTileFileSystemCacheTrimBytes((long) ((freeSpace + cacheSize) * 0.90));
        }

        Timber.d("getTileFileSystemCacheMaxBytes(): %d",
                configuration.getTileFileSystemCacheMaxBytes());
        Timber.d("getTileFileSystemCacheTrimBytes(): %d",
                configuration.getTileFileSystemCacheTrimBytes());
    }
}
import android.animation.AnimatorInflater;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import com.squareup.otto.Subscribe;
import org.osmdroid.tileprovider.modules.SqlTileWriter;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.Overlay;
import org.osmdroid.views.overlay.gestures.RotationGestureOverlay;
import org.osmdroid.views.overlay.infowindow.InfoWindow;
import javax.inject.Inject;
import de.stephanlindauer.criticalmaps.App;
import de.stephanlindauer.criticalmaps.R;
import de.stephanlindauer.criticalmaps.databinding.FragmentMapBinding;
import de.stephanlindauer.criticalmaps.events.GpsStatusChangedEvent;
import de.stephanlindauer.criticalmaps.events.NetworkConnectivityChangedEvent;
import de.stephanlindauer.criticalmaps.events.NewLocationEvent;
import de.stephanlindauer.criticalmaps.events.NewServerResponseEvent;
import de.stephanlindauer.criticalmaps.handler.ShowGpxHandler;
import de.stephanlindauer.criticalmaps.managers.LocationUpdateManager;
import de.stephanlindauer.criticalmaps.model.OtherUsersLocationModel;
import de.stephanlindauer.criticalmaps.model.OwnLocationModel;
import de.stephanlindauer.criticalmaps.overlays.LocationMarker;
import de.stephanlindauer.criticalmaps.prefs.SharedPrefsKeys;
import de.stephanlindauer.criticalmaps.provider.EventBus;
import de.stephanlindauer.criticalmaps.utils.AlertBuilder;
import de.stephanlindauer.criticalmaps.utils.MapViewUtils;
import info.metadude.android.typedpreferences.BooleanPreference;
package de.stephanlindauer.criticalmaps.fragments;







public class MapFragment extends Fragment {
    private final static String KEY_MAP_ZOOMLEVEL = "map_zoomlevel";
    private final static String KEY_MAP_POSITION = "map_position";
    private final static String KEY_MAP_ORIENTATION = "map_orientation";
    private final static String KEY_INITIAL_LOCATION_SET = "initial_location_set";

    private final static double DEFAULT_ZOOM_LEVEL = 12;
    private final static double NO_GPS_PERMISSION_ZOOM_LEVEL = 3;

    @Inject
    OwnLocationModel ownLocationModel;

    @Inject
    OtherUsersLocationModel otherUsersLocationModel;

    @Inject
    EventBus eventBus;

    @Inject
    LocationUpdateManager locationUpdateManager;

    @Inject
    ShowGpxHandler showGpxHandler;

    @Inject
    SharedPreferences sharedPreferences;

    private MapView mapView;
    private InfoWindow observerInfoWindow;

    private final GeoPoint defaultGeoPoint = new GeoPoint(52.499571, 13.4140875, 15);
    private boolean isInitialLocationSet = false;
    private ObjectAnimator gpsSearchingAnimator;

    // cache drawables
    private Drawable locationIcon;
    private Drawable ownLocationIcon;
    private Drawable ownLocationIconObserver;

    private FragmentMapBinding binding;

    private final View.OnClickListener centerLocationOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (ownLocationModel.ownLocation != null)
                animateToLocation(ownLocationModel.ownLocation);
        }
    };

    private final View.OnClickListener rotationNorthOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            float currentRotation = mapView.getMapOrientation() % 360;

            if (currentRotation == 0.0f) {
                // no animation required; also works around bug where map does a full rotation
                // because of mapView wrapping 360° to 0° while View allows 360°
                return;
            }

            if (currentRotation < 0.0f) {
                currentRotation = 360.0f + currentRotation;
                binding.mapSetNorthFab.setRotation(currentRotation);
                mapView.setMapOrientation(currentRotation);
            }

            float destinationRotation = currentRotation > 180.0f ? 360.0f : 0.0f;
            ViewCompat.animate(binding.mapSetNorthFab)
                    .rotation(destinationRotation)
                    .setDuration(300L)
                    .setUpdateListener(view -> mapView.setMapOrientation(view.getRotation()))
                    .start();
        }
    };

    private final View.OnClickListener noGpsOnClickListener = v -> AlertBuilder.show(getActivity(),
            R.string.map_no_gps_title,
            R.string.map_no_gps_text);

    private final View.OnClickListener gpsDisabledOnClickListener =
            v -> AlertBuilder.show(getActivity(),
                    R.string.map_gps_disabled_title,
                    R.string.map_gps_disabled_text);

    private final View.OnClickListener gpsNoPermissionsOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            locationUpdateManager.requestPermission();
        }
    };

    private final View.OnClickListener gpsPermissionsPermanentlyDeniedOnClickListener = v ->
            new AlertDialog.Builder(getActivity(), R.style.AlertDialogTheme)
                    .setTitle(R.string.map_gps_permissions_permanently_denied_title)
                    .setMessage(R.string.map_gps_permissions_permanently_denied_text)
                    .setNegativeButton(R.string.no, null)
                    .setPositiveButton(R.string.permissions_open_settings, (dialog, which) -> {
                        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
                                Uri.fromParts("package", getActivity().getPackageName(), null));
                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        startActivity(intent);
                    })
                    .create()
                    .show();

    private final View.OnClickListener searchingForLocationOnClickListener = v ->
            Toast.makeText(getActivity(), R.string.map_searching_for_location, Toast.LENGTH_SHORT)
                    .show();

    private final SharedPreferences.OnSharedPreferenceChangeListener observerModeOnSharedPreferenceChangeListener =
            (sharedPreferences, key) -> {
                if (SharedPrefsKeys.OBSERVER_MODE_ACTIVE.equals(key)) {
                    refreshView();
                }
            };

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);

        binding = FragmentMapBinding.inflate(inflater, container, false);

        //noinspection ConstantConditions
        locationIcon = AppCompatResources.getDrawable(getActivity(), R.drawable.ic_map_marker);
        ownLocationIcon = AppCompatResources.getDrawable(
                getActivity(), R.drawable.ic_map_marker_own);
        ownLocationIconObserver = AppCompatResources.getDrawable(
                getActivity(), R.drawable.ic_map_marker_observer);

        return binding.getRoot();
    }

    @Override
    public void onActivityCreated(final Bundle savedState) {
        super.onActivityCreated(savedState);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            adjustToWindowsInsets();
        }

        App.components().inject(this);