diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java new file mode 100644 index 000000000..05a662c71 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -0,0 +1,2403 @@ +package fr.free.nrw.commons.nearby.fragments; + +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.CUSTOM_QUERY; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED; +import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; + +import android.Manifest.permission; +import android.annotation.SuppressLint; +import android.app.ProgressDialog; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.drawable.Drawable; +import android.location.Location; +import android.location.LocationManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.provider.Settings; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.Toast; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions; +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission; +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import androidx.lifecycle.LifecycleCoroutineScope; +import androidx.lifecycle.LifecycleOwnerKt; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback; +import com.google.android.material.snackbar.Snackbar; +import com.jakewharton.rxbinding2.view.RxView; +import com.jakewharton.rxbinding3.appcompat.RxSearchView; +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.CommonsApplication.BaseLogoutListener; +import fr.free.nrw.commons.MapController.NearbyPlacesInfo; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; +import fr.free.nrw.commons.contributions.ContributionController; +import fr.free.nrw.commons.contributions.MainActivity; +import fr.free.nrw.commons.contributions.MainActivity.ActiveFragment; +import fr.free.nrw.commons.databinding.FragmentNearbyParentBinding; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.kvstore.JsonKvStore; +import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.location.LocationPermissionsHelper; +import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback; +import fr.free.nrw.commons.location.LocationServiceManager; +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; +import fr.free.nrw.commons.location.LocationUpdateListener; +import fr.free.nrw.commons.nearby.BottomSheetAdapter; +import fr.free.nrw.commons.nearby.CheckBoxTriStates; +import fr.free.nrw.commons.nearby.Label; +import fr.free.nrw.commons.nearby.MarkerPlaceGroup; +import fr.free.nrw.commons.nearby.NearbyController; +import fr.free.nrw.commons.nearby.NearbyFilterSearchRecyclerViewAdapter; +import fr.free.nrw.commons.nearby.NearbyFilterState; +import fr.free.nrw.commons.nearby.NearbyUtil; +import fr.free.nrw.commons.nearby.Place; +import fr.free.nrw.commons.nearby.PlacesRepository; +import fr.free.nrw.commons.nearby.Sitelinks; +import fr.free.nrw.commons.nearby.WikidataFeedback; +import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract; +import fr.free.nrw.commons.nearby.fragments.AdvanceQueryFragment.Callback; +import fr.free.nrw.commons.nearby.model.BottomSheetItem; +import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter; +import fr.free.nrw.commons.upload.FileUtils; +import fr.free.nrw.commons.utils.DialogUtil; +import fr.free.nrw.commons.utils.ExecutorUtils; +import fr.free.nrw.commons.utils.LayoutUtils; +import fr.free.nrw.commons.utils.MapUtils; +import fr.free.nrw.commons.utils.NearbyFABUtils; +import fr.free.nrw.commons.utils.NetworkUtils; +import fr.free.nrw.commons.utils.SystemThemeUtils; +import fr.free.nrw.commons.utils.ViewUtil; +import fr.free.nrw.commons.wikidata.WikidataEditListener; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import javax.inject.Named; +import kotlin.Unit; +import kotlinx.coroutines.BuildersKt; +import org.jetbrains.annotations.NotNull; +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.events.MapEventsReceiver; +import org.osmdroid.events.MapListener; +import org.osmdroid.events.ScrollEvent; +import org.osmdroid.events.ZoomEvent; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.util.constants.GeoConstants.UnitOfMeasure; +import org.osmdroid.views.CustomZoomButtonsController.Visibility; +import org.osmdroid.views.overlay.MapEventsOverlay; +import org.osmdroid.views.overlay.Marker; +import org.osmdroid.views.overlay.Overlay; +import org.osmdroid.views.overlay.ScaleBarOverlay; +import org.osmdroid.views.overlay.ScaleDiskOverlay; +import org.osmdroid.views.overlay.TilesOverlay; +import timber.log.Timber; + + +public class NearbyParentFragment extends CommonsDaggerSupportFragment + implements NearbyParentFragmentContract.View, + WikidataEditListener.WikidataP18EditListener, LocationUpdateListener, + LocationPermissionCallback, BottomSheetAdapter.ItemClickListener { + + FragmentNearbyParentBinding binding; + + public final MapEventsOverlay mapEventsOverlay = new MapEventsOverlay(new MapEventsReceiver() { + @Override + public boolean singleTapConfirmedHelper(GeoPoint p) { + if (clickedMarker != null) { + clickedMarker.closeInfoWindow(); + } else { + Timber.e("CLICKED MARKER IS NULL"); + } + if (isListBottomSheetExpanded()) { + // Back should first hide the bottom sheet if it is expanded + hideBottomSheet(); + } else if (isDetailsBottomSheetVisible()) { + hideBottomDetailsSheet(); + } + return true; + } + + @Override + public boolean longPressHelper(GeoPoint p) { + return false; + } + }); + + @Inject + LocationServiceManager locationManager; + @Inject + NearbyController nearbyController; + @Inject + @Named("default_preferences") + JsonKvStore applicationKvStore; + @Inject + BookmarkLocationsDao bookmarkLocationDao; + @Inject + PlacesRepository placesRepository; + @Inject + ContributionController controller; + @Inject + WikidataEditListener wikidataEditListener; + @Inject + SystemThemeUtils systemThemeUtils; + @Inject + CommonPlaceClickActions commonPlaceClickActions; + + private LocationPermissionsHelper locationPermissionsHelper; + private NearbyFilterSearchRecyclerViewAdapter nearbyFilterSearchRecyclerViewAdapter; + private BottomSheetBehavior bottomSheetListBehavior; + private BottomSheetBehavior bottomSheetDetailsBehavior; + private Animation rotate_backward; + private Animation fab_close; + private Animation fab_open; + private Animation rotate_forward; + private static final float ZOOM_LEVEL = 15f; + private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; + private BroadcastReceiver broadcastReceiver; + private boolean isNetworkErrorOccurred; + private Snackbar snackbar; + private View view; + private LifecycleCoroutineScope scope; + private NearbyParentFragmentPresenter presenter; + private boolean isDarkTheme; + private boolean isFABsExpanded; + private Place selectedPlace; + private Marker clickedMarker; + private ProgressDialog progressDialog; + private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.005; + private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.004; + private boolean isPermissionDenied; + private boolean recenterToUserLocation; + private GeoPoint mapCenter; + IntentFilter intentFilter = new IntentFilter(NETWORK_INTENT_ACTION); + private Place lastPlaceToCenter; + private LatLng lastKnownLocation; + private boolean isVisibleToUser; + private LatLng lastFocusLocation; + private PlaceAdapter adapter; + private GeoPoint lastMapFocus; + private NearbyParentFragmentInstanceReadyCallback nearbyParentFragmentInstanceReadyCallback; + private boolean isAdvancedQueryFragmentVisible = false; + private Place nearestPlace; + private volatile boolean stopQuery; + + private final Handler searchHandler = new Handler(); + private Runnable searchRunnable; + + private LatLng updatedLatLng; + private boolean searchable; + + private ConstraintLayout nearbyLegend; + + private GridLayoutManager gridLayoutManager; + private List dataList; + private BottomSheetAdapter bottomSheetAdapter; + + private final ActivityResultLauncher galleryPickLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { + controller.onPictureReturnedFromGallery(result, requireActivity(), callbacks); + }); + }); + + private final ActivityResultLauncher customSelectorLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { + controller.onPictureReturnedFromCustomSelector(result, requireActivity(), callbacks); + }); + }); + + private final ActivityResultLauncher cameraPickLauncherForResult = + registerForActivityResult(new StartActivityForResult(), + result -> { + controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { + controller.onPictureReturnedFromCamera(result, requireActivity(), callbacks); + }); + }); + + private ActivityResultLauncher inAppCameraLocationPermissionLauncher = registerForActivityResult( + new RequestMultiplePermissions(), + new ActivityResultCallback>() { + @Override + public void onActivityResult(Map result) { + boolean areAllGranted = true; + for (final boolean b : result.values()) { + areAllGranted = areAllGranted && b; + } + + if (areAllGranted) { + controller.locationPermissionCallback.onLocationPermissionGranted(); + } else { + if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { + controller.handleShowRationaleFlowCameraLocation(getActivity(), + inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); + } else { + controller.locationPermissionCallback.onLocationPermissionDenied( + getActivity().getString( + R.string.in_app_camera_location_permission_denied)); + } + } + } + }); + + private ActivityResultLauncher locationPermissionLauncher = registerForActivityResult( + new RequestPermission(), isGranted -> { + if (isGranted) { + locationPermissionGranted(); + } else { + if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { + DialogUtil.showAlertDialog(getActivity(), + getActivity().getString(R.string.location_permission_title), + getActivity().getString(R.string.location_permission_rationale_nearby), + getActivity().getString(android.R.string.ok), + getActivity().getString(android.R.string.cancel), + () -> { + askForLocationPermission(); + }, + null, + null + ); + } else { + if (isPermissionDenied) { + locationPermissionsHelper.showAppSettingsDialog(getActivity(), + R.string.nearby_needs_location); + } + Timber.d("The user checked 'Don't ask again' or denied the permission twice"); + isPermissionDenied = true; + } + } + }); + + /** + * WLM URL + */ + public static final String WLM_URL = "https://commons.wikimedia.org/wiki/Commons:Mobile_app/Contributing_to_WLM_using_the_app"; + + @NonNull + public static NearbyParentFragment newInstance() { + NearbyParentFragment fragment = new NearbyParentFragment(); + fragment.setRetainInstance(true); + return fragment; + } + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { + binding = FragmentNearbyParentBinding.inflate(inflater, container, false); + view = binding.getRoot(); + + initNetworkBroadCastReceiver(); + scope = LifecycleOwnerKt.getLifecycleScope(getViewLifecycleOwner()); + presenter = new NearbyParentFragmentPresenter(bookmarkLocationDao, placesRepository, nearbyController); + progressDialog = new ProgressDialog(getActivity()); + progressDialog.setCancelable(false); + progressDialog.setMessage("Saving in progress..."); + setHasOptionsMenu(true); + + // Inflate the layout for this fragment + return view; + + } + + @Override + public void onCreateOptionsMenu(@NonNull final Menu menu, + @NonNull final MenuInflater inflater) { + inflater.inflate(R.menu.nearby_fragment_menu, menu); + MenuItem refreshButton = menu.findItem(R.id.item_refresh); + MenuItem listMenu = menu.findItem(R.id.list_sheet); + MenuItem saveAsGPXButton = menu.findItem(R.id.list_item_gpx); + MenuItem saveAsKMLButton = menu.findItem(R.id.list_item_kml); + refreshButton.setOnMenuItemClickListener(new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + try { + emptyCache(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return false; + } + }); + listMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + listOptionMenuItemClicked(); + return false; + } + }); + saveAsGPXButton.setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(@NonNull MenuItem item) { + try { + progressDialog.setTitle(getString(R.string.saving_gpx_file)); + progressDialog.show(); + savePlacesAsGPX(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return false; + } + }); + saveAsKMLButton.setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(@NonNull MenuItem item) { + try { + progressDialog.setTitle(getString(R.string.saving_kml_file)); + progressDialog.show(); + savePlacesAsKML(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return false; + } + }); + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + isDarkTheme = systemThemeUtils.isDeviceInNightMode(); + if (Utils.isMonumentsEnabled(new Date())) { + binding.rlContainerWlmMonthMessage.setVisibility(View.VISIBLE); + } else { + binding.rlContainerWlmMonthMessage.setVisibility(View.GONE); + } + locationPermissionsHelper = new LocationPermissionsHelper(getActivity(), locationManager, + this); + + // Set up the floating activity button to toggle the visibility of the legend + binding.fabLegend.setOnClickListener(v -> { + if (binding.nearbyLegendLayout.getRoot().getVisibility() == View.VISIBLE) { + binding.nearbyLegendLayout.getRoot().setVisibility(View.GONE); + } else { + binding.nearbyLegendLayout.getRoot().setVisibility(View.VISIBLE); + } + }); + + presenter.attachView(this); + isPermissionDenied = false; + recenterToUserLocation = false; + initThemePreferences(); + initViews(); + presenter.setActionListeners(applicationKvStore); + org.osmdroid.config.Configuration.getInstance().load(this.getContext(), + PreferenceManager.getDefaultSharedPreferences(this.getContext())); + + // Use the Wikimedia tile server, rather than OpenStreetMap (Mapnik) which has various + // restrictions that we do not satisfy. + binding.map.setTileSource(TileSourceFactory.WIKIMEDIA); + binding.map.setTilesScaledToDpi(true); + + // Add referer HTTP header because the Wikimedia tile server requires it. + // This was suggested by Dmitry Brant within an email thread between us and WMF. + org.osmdroid.config.Configuration.getInstance().getAdditionalHttpRequestProperties().put( + "Referer", "http://maps.wikimedia.org/" + ); + + if (applicationKvStore.getString("LastLocation") + != null) { // Checking for last searched location + String[] locationLatLng = applicationKvStore.getString("LastLocation").split(","); + lastMapFocus = new GeoPoint(Double.valueOf(locationLatLng[0]), + Double.valueOf(locationLatLng[1])); + } else { + lastMapFocus = new GeoPoint(51.50550, -0.07520); + } + ScaleBarOverlay scaleBarOverlay = new ScaleBarOverlay(binding.map); + scaleBarOverlay.setScaleBarOffset(15, 25); + Paint barPaint = new Paint(); + barPaint.setARGB(200, 255, 250, 250); + scaleBarOverlay.setBackgroundPaint(barPaint); + scaleBarOverlay.enableScaleBar(); + binding.map.getOverlays().add(scaleBarOverlay); + binding.map.getZoomController().setVisibility(Visibility.NEVER); + binding.map.getController().setZoom(ZOOM_LEVEL); + binding.map.getOverlays().add(mapEventsOverlay); + + binding.map.addMapListener(new MapListener() { + @Override + public boolean onScroll(ScrollEvent event) { + presenter.handleMapScrolled(scope, !isNetworkErrorOccurred); + return true; + } + + @Override + public boolean onZoom(ZoomEvent event) { + return false; + } + + }); + + binding.map.setMultiTouchControls(true); + if (nearbyParentFragmentInstanceReadyCallback != null) { + nearbyParentFragmentInstanceReadyCallback.onReady(); + } + initNearbyFilter(); + addCheckBoxCallback(); + moveCameraToPosition(lastMapFocus); + initRvNearbyList(); + onResume(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution), Html.FROM_HTML_MODE_LEGACY)); + } else { + //noinspection deprecation + binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution))); + } + binding.tvAttribution.setMovementMethod(LinkMovementMethod.getInstance()); + binding.nearbyFilterList.btnAdvancedOptions.setOnClickListener(v -> { + binding.nearbyFilter.searchViewLayout.searchView.clearFocus(); + showHideAdvancedQueryFragment(true); + final AdvanceQueryFragment fragment = new AdvanceQueryFragment(); + final Bundle bundle = new Bundle(); + try { + bundle.putString("query", + FileUtils.INSTANCE.readFromResource( + "/queries/radius_query_for_upload_wizard.rq") + ); + } catch (IOException e) { + Timber.e(e); + } + fragment.setArguments(bundle); + fragment.callback = new Callback() { + @Override + public void close() { + showHideAdvancedQueryFragment(false); + } + + @Override + public void reset() { + presenter.setAdvancedQuery(null); + presenter.updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED); + showHideAdvancedQueryFragment(false); + } + + @Override + public void apply(@NotNull final String query) { + presenter.setAdvancedQuery(query); + presenter.updateMapAndList(CUSTOM_QUERY); + showHideAdvancedQueryFragment(false); + } + }; + getChildFragmentManager().beginTransaction() + .replace(R.id.fl_container_nearby_children, fragment) + .commit(); + }); + + binding.tvLearnMore.setOnClickListener(v -> onLearnMoreClicked()); + + if (!locationPermissionsHelper.checkLocationPermission(getActivity())) { + askForLocationPermission(); + } + } + + /** + * Initialise background based on theme, this should be doe ideally via styles, that would need + * another refactor + */ + private void initThemePreferences() { + if (isDarkTheme) { + binding.bottomSheetNearby.rvNearbyList.setBackgroundColor( + getContext().getResources().getColor(R.color.contributionListDarkBackground)); + binding.nearbyFilterList.checkboxTriStates.setTextColor( + getContext().getResources().getColor(android.R.color.white)); + binding.nearbyFilterList.checkboxTriStates.setTextColor( + getContext().getResources().getColor(android.R.color.white)); + binding.nearbyFilterList.getRoot().setBackgroundColor( + getContext().getResources().getColor(R.color.contributionListDarkBackground)); + binding.map.getOverlayManager().getTilesOverlay() + .setColorFilter(TilesOverlay.INVERT_COLORS); + } else { + binding.bottomSheetNearby.rvNearbyList.setBackgroundColor( + getContext().getResources().getColor(android.R.color.white)); + binding.nearbyFilterList.checkboxTriStates.setTextColor( + getContext().getResources().getColor(R.color.contributionListDarkBackground)); + binding.nearbyFilterList.getRoot().setBackgroundColor( + getContext().getResources().getColor(android.R.color.white)); + binding.nearbyFilterList.getRoot().setBackgroundColor( + getContext().getResources().getColor(android.R.color.white)); + } + } + + private void initRvNearbyList() { + binding.bottomSheetNearby.rvNearbyList.setLayoutManager( + new LinearLayoutManager(getContext())); + adapter = new PlaceAdapter(bookmarkLocationDao, + scope, + place -> { + moveCameraToPosition( + new GeoPoint(place.location.getLatitude(), place.location.getLongitude())); + return Unit.INSTANCE; + }, + (place, isBookmarked) -> { + presenter.toggleBookmarkedStatus(place, scope); + return Unit.INSTANCE; + }, + commonPlaceClickActions, + inAppCameraLocationPermissionLauncher, + galleryPickLauncherForResult, + cameraPickLauncherForResult + ); + binding.bottomSheetNearby.rvNearbyList.setAdapter(adapter); + } + + private void addCheckBoxCallback() { + binding.nearbyFilterList.checkboxTriStates.setCallback( + (o, state, b, b1) -> presenter.filterByMarkerType(o, state, b, b1)); + } + + private void performMapReadyActions() { + if (((MainActivity) getActivity()).activeFragment == ActiveFragment.NEARBY) { + if (applicationKvStore.getBoolean("doNotAskForLocationPermission", false) && + !locationPermissionsHelper.checkLocationPermission(getActivity())) { + isPermissionDenied = true; + } + } + presenter.onMapReady(); + } + + @Override + public void askForLocationPermission() { + Timber.d("Asking for location permission"); + locationPermissionLauncher.launch(permission.ACCESS_FINE_LOCATION); + } + + private void locationPermissionGranted() { + isPermissionDenied = false; + applicationKvStore.putBoolean("doNotAskForLocationPermission", false); + lastKnownLocation = locationManager.getLastLocation(); + LatLng target = lastKnownLocation; + if (lastKnownLocation != null) { + GeoPoint targetP = new GeoPoint(target.getLatitude(), target.getLongitude()); + mapCenter = targetP; + binding.map.getController().setCenter(targetP); + recenterMarkerToPosition(targetP); + moveCameraToPosition(targetP); + } else if (locationManager.isGPSProviderEnabled() + || locationManager.isNetworkProviderEnabled()) { + locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER); + locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); + setProgressBarVisibility(true); + } else { + locationPermissionsHelper.showLocationOffDialog(getActivity(), + R.string.ask_to_turn_location_on_text); + } + presenter.onMapReady(); + registerUnregisterLocationListener(false); + } + + @Override + public void onResume() { + super.onResume(); + binding.map.onResume(); + presenter.attachView(this); + registerNetworkReceiver(); + if (isResumed() && ((MainActivity) getActivity()).activeFragment == ActiveFragment.NEARBY) { + if (locationPermissionsHelper.checkLocationPermission(getActivity())) { + locationPermissionGranted(); + } else { + startMapWithoutPermission(); + } + } + } + + /** + * Starts the map without GPS and without permission By default it points to 51.50550,-0.07520 + * coordinates, other than that it points to the last known location which can be get by the key + * "LastLocation" from applicationKvStore + */ + private void startMapWithoutPermission() { + if (applicationKvStore.getString("LastLocation") != null) { + final String[] locationLatLng + = applicationKvStore.getString("LastLocation").split(","); + lastKnownLocation + = new LatLng(Double.parseDouble(locationLatLng[0]), + Double.parseDouble(locationLatLng[1]), 1f); + } else { + lastKnownLocation = MapUtils.getDefaultLatLng(); + } + if (binding.map != null) { + moveCameraToPosition( + new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); + } + presenter.onMapReady(); + } + + private void registerNetworkReceiver() { + if (getActivity() != null) { + getActivity().registerReceiver(broadcastReceiver, intentFilter); + } + } + + @Override + public void onPause() { + super.onPause(); + binding.map.onPause(); + compositeDisposable.clear(); + presenter.detachView(); + registerUnregisterLocationListener(true); + try { + if (broadcastReceiver != null && getActivity() != null) { + getContext().unregisterReceiver(broadcastReceiver); + } + + if (locationManager != null && presenter != null) { + locationManager.removeLocationListener(presenter); + locationManager.unregisterLocationManager(); + } + } catch (final Exception e) { + Timber.e(e); + //Broadcast receivers should always be unregistered inside catch, you never know if they were already registered or not + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + searchHandler.removeCallbacks(searchRunnable); + presenter.removeNearbyPreferences(applicationKvStore); + } + + private void initViews() { + Timber.d("init views called"); + initBottomSheets(); + loadAnimations(); + setBottomSheetCallbacks(); + addActionToTitle(); + if (!Utils.isMonumentsEnabled(new Date())) { + NearbyFilterState.setWlmSelected(false); + } + } + + /** + * a) Creates bottom sheet behaviours from bottom sheets, sets initial states and visibility b) + * Gets the touch event on the map to perform following actions: if fab is open then close fab. + * if bottom sheet details are expanded then collapse bottom sheet details. if bottom sheet + * details are collapsed then hide the bottom sheet details. if listBottomSheet is open then + * hide the list bottom sheet. + */ + @SuppressLint("ClickableViewAccessibility") + private void initBottomSheets() { + bottomSheetListBehavior = BottomSheetBehavior.from(binding.bottomSheetNearby.bottomSheet); + bottomSheetDetailsBehavior = BottomSheetBehavior.from(binding.bottomSheetDetails.getRoot()); + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + binding.bottomSheetDetails.getRoot().setVisibility(View.VISIBLE); + bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + + /** + * Determines the number of spans (columns) in the RecyclerView based on device orientation + * and adapter item count. + * + * @return The number of spans to be used in the RecyclerView. + */ + private int getSpanCount() { + int orientation = getResources().getConfiguration().orientation; + if (bottomSheetAdapter != null) { + return (orientation == Configuration.ORIENTATION_PORTRAIT) ? 3 + : bottomSheetAdapter.getItemCount(); + } else { + return (orientation == Configuration.ORIENTATION_PORTRAIT) ? 3 : 6; + } + } + + public void initNearbyFilter() { + binding.nearbyFilterList.getRoot().setVisibility(View.GONE); + hideBottomSheet(); + binding.nearbyFilter.searchViewLayout.searchView.setOnQueryTextFocusChangeListener( + (v, hasFocus) -> { + LayoutUtils.setLayoutHeightAlignedToWidth(1.25, + binding.nearbyFilterList.getRoot()); + if (hasFocus) { + binding.nearbyFilterList.getRoot().setVisibility(View.VISIBLE); + presenter.searchViewGainedFocus(); + } else { + binding.nearbyFilterList.getRoot().setVisibility(View.GONE); + } + }); + binding.nearbyFilterList.searchListView.setHasFixedSize(true); + binding.nearbyFilterList.searchListView.addItemDecoration( + new DividerItemDecoration(getContext(), + DividerItemDecoration.VERTICAL)); + final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity()); + linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); + binding.nearbyFilterList.searchListView.setLayoutManager(linearLayoutManager); + nearbyFilterSearchRecyclerViewAdapter = new NearbyFilterSearchRecyclerViewAdapter( + getContext(), new ArrayList<>(Label.valuesAsList()), + binding.nearbyFilterList.searchListView); + nearbyFilterSearchRecyclerViewAdapter.setCallback( + new NearbyFilterSearchRecyclerViewAdapter.Callback() { + @Override + public void setCheckboxUnknown() { + presenter.setCheckboxUnknown(); + } + + @Override + public void filterByMarkerType(final ArrayList