Refactor nearby classes mvp (#2969)

* Create parent contract

* Create map child contract and fill methods

* Add javadocs and specific interfaces for list

* Move general method to parent and add javadocs for parent

* Add explanation for keeping an emty View interface under NearbyListContract

* Move constracts under contract package

* Create presenters for map and list and implement user actions accordingly

* Add javadocs

* Add presenter, contract and fragment for parent Fragment of both NearyListFragment and NearbyMapFragment

* Implement missing methods

* Fix typo

* Add main views on fragment

* İmplement child fragment logic and their retain

* Relate parent presenter with parent fragment

* Add all location permission related methods to view contract and implement in fragment. Call them from presenter by passing locationServiceManager parameter

* Define refreshView method as updateMapAndList which is a better naming. Define it at presenter part.

* Define a presenter variable in fragment and call updateMapAndList method from there, if permissions are okay

* Add lock neabry method to unlisten nearby operations during updates

* Add network connection established check on view side, check it from presenter

* try to simplify previous method during refactor

* Add missing methods for NearbyMap

* Connect child fragment and prent fragment with presenter

* Change nearby design, first create views then register listeners

* AddnetworkBroadcatsReceiver on view side, call it from presenter

* Add comments

* Change the old NearbyFragment by our new NearbyParentFragment for first tests

* Prevent crash caused child fragment is actually null by checking if it is attached or not.

* Makes sure that initialize nearby operations method is called just after all views and fragments are ready and attached

* Make sure updateMAoAndList method is called when everything ready

* Rename a method with prepoer name

* Call update map and add required markers

* Find out zoom level problem

* Implement add nearby markers and add current marker methods

* remove unneeded codes copied from previous implementation

* Revert "remove unneeded codes copied from previous implementation"

This reverts commit 42539651de.

* add some commits and clear code

* Remove location listener implementation from view, handle them in presenter instead. And add timber debug notes and required method calls

* Style ,issues

* Refactor a variable name to camel case and bind search this area view

* Search this area button action is added

* Mostly implement search this area methods, not tested yet even once

* Make sure everything is called in required order and seach this area method basically works

* Rename methods accordingly and add Javadocs

* Add current location marker and remove circle around it

* Remove unused methods

* Add current location marker with object animator and remove previous marker is exists

* include clear map into add markers method and reorder methods

* Make search this area button appear at correct time

* Try to load un search this area is called

* Clear logs

* Add changes for permission made by Vivek to newly added fragments along with our nearby classes

* Add a view to nearby map ragment and insert map view as an item inside it

* Add logs to uınderstand why on meap ready callback is never called

* Add list item clicked and bottom sheet for list of nearby items is expanded

* Make nearby map ready callback came

* Add required methods to be called after map view is ready

* State: Map ready call is not called but permissions and methods are in correct order

* Remove unused logs

* Try to use SupportMapFragment instead, still no success...

* use SupportMapFragment instead

* Remove unused Near

* Upgrade mapbox sdk versions

* Remove Style import from fragment/NearbyMapFragment

* Remove Style import from **fragments/NearbyParentFragment

* Remove nearby/NearbyMapFragment

* Remove unused/old NearbyFragment and NearbyMapFragment

* Remove import of already removed class

* Make sure you removed everything related with mapbox implementation

* Update mapbox map

* Remove unused classes, do not forget centerMapToPlace doesn't work on this branch

* Remove Style class it required updated version of mapbox map

* Add base codes for new activity

* Prove that our mapbox sdk let us implement the map directly inside the activity

* Add base codes for testing mapbox activity in a single fragment inside activity

* Add codes from mapbox demo repository to test support fragment, map works in a single layer fragment

* Add base codes for test layered fragment activity

* Add Support fragment inside a fragment and proves that layered fragmentw with map works

* Test view pager and tab layout with support map fragment to see it works, it works!

* Move Contributions Fragment related codes to test activity

* Move nearby card methods

* Inject location manager and implement NerabyParentFragmentContract.View on test fragment

* Coppy the content of SupportMapFragment from mapbox repository, use this code to modify later. This method war suggested by mapbox team instead of extending the class

* Implement NearbyMapContract.View on our new SupportMapFragment

* Start to mplement logic of checking permissions

* Fix small dagger issue to inject location manager properly

* Request permission for nearby places if fragment is loaded and tab is selected

* Initialize map operations if map ready and tab is selected

* Markers loads at correct time

* Style the map according to new version of mapbox map

* Add some map elements like FABs and give their actions

* Implement map marker click actions

* Implement nearby Fabs logic with a small issue

* fix FABs are not closing problem

* Unkown problem occurs at map load when I try to use MainActivity again.

* Revert "Unkown problem occurs at map load when I try to use MainActivity again."

This reverts commit 3dc084415b.

* Search this area buttons are added but button function does not work and button visibility is problematic

* Fix issue with MainActivity with the help of Ashish

* Fix search this area button visibility issue

* Fix the issues with updating search nearby markers and camera position together

* Fix progress bar visibility issue

* Prevent loding map each time tab selected

* Take toolbar back

* Implement back button with presenter

* Add click actoion to bottom sheet details

* Add nearby list into bottom sheeet

* Make reuse existing fragments if there is any

* Code cleanup

* Cleanup

* Code cleanup

* Add lifecyle codes to prevent leaks

* Cleanup

* Code cleanup

* cleanup

* Make list item clicked and map focus to same place

* Add bookmark from list fragment

* Make nearby card click action work

* Revert "Fix conflicts"

This reverts commit f3451745d3, reversing
changes made to c5d4d5533d.

* Code cleanup

* Cleanup

* Make recenter button work when list sheet is expanded

* Cleanup

* Code cleanups

* NPE issue is not detected for now, can be solved on seperate PR

* Update map after Wikidata edit

* Cherry picked previously reverted merge, hoping this will fix enourmus amounts of diff files

* Previous merge bringed an file which should be deleted. Delete it.

* Revert irrelevant changes on build.gradle

* Revert irrelevant changes

* Jetbrains annotation is not included to build gradle, this issue caused build issues on my branch so I removed it to be able to build

* Rename ListView

* Use a singleton to access the presenter, instead of passing it to a variable inside fragment

* Fix code style issues

* Move hardcoded colors to colors.xml file

* Make larger methods smaller

* Make current location marker follow

* Do not track users position if user is searching around

* Revert irrelevant shanges at build gradle

* Remove unneeded variable

* Remove mvp directory, add sub directories directly under nearby directory instead

* Remove unneeded public definiton

* Remove unneeded namespace

* Add public defiiton, it is needed to reach the constructor.
This commit is contained in:
neslihanturan 2019-10-15 17:32:05 +03:00 committed by Josephine Lim
parent 8a2931270c
commit 958bbbdd81
24 changed files with 1995 additions and 1938 deletions

View file

@ -36,7 +36,8 @@ dependencies {
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation 'com.github.pedrovgs:renderers:3.3.3'
implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-localization:0.6.0'
implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:7.2.0'
implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-localization-v7:0.7.0'
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0'
@ -162,7 +163,7 @@ android {
versionNameSuffix "-debug-" + getBranchName()
}
}
if (isRunningOnTravisAndIsNotPRBuild) {
// configure keystore based on env vars in Travis for automated alpha builds
signingConfigs.release.storeFile = file("../nr-commons.keystore")

View file

@ -34,8 +34,9 @@ import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.nearby.NearbyFragment;
import fr.free.nrw.commons.nearby.NearbyNotificationCardView;
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter;
import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.notification.NotificationController;
@ -50,13 +51,15 @@ import static android.content.ContentResolver.requestSync;
public class MainActivity extends NavigationBaseActivity implements FragmentManager.OnBackStackChangedListener {
@Inject
SessionManager sessionManager;
@Inject ContributionController controller;
@BindView(R.id.tab_layout)
TabLayout tabLayout;
@BindView(R.id.pager)
public UnswipableViewPager viewPager;
@Inject
SessionManager sessionManager;
@Inject
ContributionController controller;
@Inject
public LocationServiceManager locationManager;
@Inject
@ -72,10 +75,9 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
public static final int NEARBY_TAB_POSITION = 1;
public boolean isContributionsFragmentVisible = true; // False means nearby fragment is visible
public boolean onOrientationChanged;
private Menu menu;
private boolean onOrientationChanged = false;
private MenuItem notificationsMenuItem;
private TextView notificationCount;
@ -168,9 +170,7 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
* tab won't change and vice versa. So we have to notify each of them.
*/
private void setTabAndViewPagerSynchronisation() {
//viewPager.canScrollHorizontally(false);
viewPager.setFocusableInTouchMode(true);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
@ -185,7 +185,6 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
tabLayout.getTabAt(CONTRIBUTIONS_TAB_POSITION).select();
isContributionsFragmentVisible = true;
updateMenuItem();
break;
case NEARBY_TAB_POSITION:
Timber.d("Nearby tab selected");
@ -193,7 +192,7 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
isContributionsFragmentVisible = false;
updateMenuItem();
// Do all permission and GPS related tasks on tab selected, not on create
((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).onTabSelected(onOrientationChanged);
NearbyParentFragmentPresenter.getInstance().onTabSelected();
break;
default:
tabLayout.getTabAt(CONTRIBUTIONS_TAB_POSITION).select();
@ -269,15 +268,7 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
}
} else if (getSupportFragmentManager().findFragmentByTag(nearbyFragmentTag) != null && !isContributionsFragmentVisible) {
// Means that nearby fragment is visible (not contributions fragment)
NearbyFragment nearbyFragment = (NearbyFragment) contributionsActivityPagerAdapter.getItem(1);
if(nearbyFragment.isBottomSheetExpanded()) {
// Back should first hide the bottom sheet if it is expanded
nearbyFragment.listOptionMenuItemClicked();
} else {
// Otherwise go back to contributions fragment
viewPager.setCurrentItem(0);
}
NearbyParentFragmentPresenter.getInstance().backButtonClicked();
} else {
super.onBackPressed();
}
@ -334,12 +325,12 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
// Display notifications menu item
menu.findItem(R.id.notifications).setVisible(true);
menu.findItem(R.id.list_sheet).setVisible(false);
Timber.d("Contributions activity notifications menu item is visible");
Timber.d("Contributions fragment notifications menu item is visible");
} else {
// Display bottom list menu item
menu.findItem(R.id.notifications).setVisible(false);
menu.findItem(R.id.list_sheet).setVisible(true);
Timber.d("Contributions activity list sheet menu item is visible");
Timber.d("Nearby fragment list sheet menu item is visible");
}
}
}
@ -353,7 +344,7 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
return true;
case R.id.list_sheet:
if (contributionsActivityPagerAdapter.getItem(1) != null) {
((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).listOptionMenuItemClicked();
((NearbyParentFragment)contributionsActivityPagerAdapter.getItem(1)).listOptionMenuItemClicked();
}
return true;
default:
@ -363,8 +354,6 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
public class ContributionsActivityPagerAdapter extends FragmentPagerAdapter {
FragmentManager fragmentManager;
private boolean isContributionsListFragment = true; // to know what to put in first tab, Contributions of Media Details
public ContributionsActivityPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
@ -394,12 +383,12 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
}
case 1:
NearbyFragment retainedNearbyFragment = getNearbyFragment(1);
NearbyParentFragment retainedNearbyFragment = getNearbyFragment(1);
if (retainedNearbyFragment != null) {
return retainedNearbyFragment;
} else {
// If we reach here, retainedNearbyFragment is null
return new NearbyFragment();
return new NearbyParentFragment();
}
default:
return null;
@ -421,9 +410,9 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
* @param position index of tabs, in our case 0 or 1
* @return
*/
private NearbyFragment getNearbyFragment(int position) {
private NearbyParentFragment getNearbyFragment(int position) {
String tag = makeFragmentName(R.id.pager, position);
return (NearbyFragment)fragmentManager.findFragmentByTag(tag);
return (NearbyParentFragment)fragmentManager.findFragmentByTag(tag);
}
/**
@ -446,7 +435,6 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
controller.handleActivityResult(this, requestCode, resultCode, data);
}
@Override
protected void onResume() {
super.onResume();
setNotificationCount();

View file

@ -13,9 +13,9 @@ import fr.free.nrw.commons.explore.images.SearchImageFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.nearby.NearbyFragment;
import fr.free.nrw.commons.nearby.NearbyListFragment;
import fr.free.nrw.commons.nearby.NearbyMapFragment;
import fr.free.nrw.commons.nearby.fragments.NearbyListFragment;
import fr.free.nrw.commons.nearby.fragments.NearbyMapFragment;
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
import fr.free.nrw.commons.review.ReviewImageFragment;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
@ -38,9 +38,6 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract NearbyListFragment bindNearbyListFragment();
@ContributesAndroidInjector
abstract NearbyMapFragment bindNearbyMapFragment();
@ContributesAndroidInjector
abstract SettingsFragment bindSettingsFragment();
@ -63,7 +60,10 @@ public abstract class FragmentBuilderModule {
abstract ContributionsFragment bindContributionsFragment();
@ContributesAndroidInjector
abstract NearbyFragment bindNearbyFragment();
abstract NearbyMapFragment bindNearbyMapFragment();
@ContributesAndroidInjector
abstract NearbyParentFragment bindNearbyParentFragment();
@ContributesAndroidInjector
abstract BookmarkPicturesFragment bindBookmarkPictureListFragment();

View file

@ -6,7 +6,6 @@ import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -222,6 +221,7 @@ public class LocationServiceManager implements LocationListener {
LOCATION_MEDIUM_CHANGED, //Between slight and significant changes, will be used for nearby card view updates.
LOCATION_NOT_CHANGED,
PERMISSION_JUST_GRANTED,
MAP_UPDATED
MAP_UPDATED,
SEARCH_CUSTOM_AREA
}
}

View file

@ -23,7 +23,7 @@ public class NearbyBaseMarker extends BaseMarkerOptions<NearbyMarker, NearbyBase
private Place place;
NearbyBaseMarker() {
public NearbyBaseMarker() {
}
private NearbyBaseMarker(Parcel in) {

View file

@ -28,8 +28,10 @@ import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
public class NearbyController {
private static final int MAX_RESULTS = 1000;
private final NearbyPlaces nearbyPlaces;
public static double searchedRadius = 10.0; //in kilometers
public static LatLng currentLocation;
public static double currentLocationSearchRadius = 10.0; //in kilometers
public static LatLng currentLocation; // Users latest fetched location
public static LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
public static double latestSearchRadius = 10.0; // Any last search radius except closest result search
@Inject
public NearbyController(NearbyPlaces nearbyPlaces) {
@ -41,21 +43,21 @@ public class NearbyController {
* Prepares Place list to make their distance information update later.
*
* @param curLatLng current location for user
* @param latLangToSearchAround the location user wants to search around
* @param searchLatLng the location user wants to search around
* @param returnClosestResult if this search is done to find closest result or all results
* @return NearbyPlacesInfo a variable holds Place list without distance information
* and boundary coordinates of current Place List
*/
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng, LatLng latLangToSearchAround, boolean returnClosestResult, boolean checkingAroundCurrentLocation) throws IOException {
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng, LatLng searchLatLng, boolean returnClosestResult, boolean checkingAroundCurrentLocation) throws IOException {
Timber.d("Loading attractions near %s", latLangToSearchAround);
Timber.d("Loading attractions near %s", searchLatLng);
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();
if (latLangToSearchAround == null) {
if (searchLatLng == null) {
Timber.d("Loading attractions nearby, but curLatLng is null");
return null;
}
List<Place> places = nearbyPlaces.radiusExpander(latLangToSearchAround, Locale.getDefault().getLanguage(), returnClosestResult);
List<Place> places = nearbyPlaces.radiusExpander(searchLatLng, Locale.getDefault().getLanguage(), returnClosestResult);
if (null != places && places.size() > 0) {
LatLng[] boundaryCoordinates = {places.get(0).location, // south
@ -91,13 +93,25 @@ public class NearbyController {
}
);
}
nearbyPlacesInfo.curLatLng = curLatLng;
nearbyPlacesInfo.searchLatLng = searchLatLng;
nearbyPlacesInfo.placeList = places;
nearbyPlacesInfo.boundaryCoordinates = boundaryCoordinates;
if (!returnClosestResult && checkingAroundCurrentLocation) {
// Do not update searched radius, if controller is used for nearby card notification
searchedRadius = nearbyPlaces.radius;
currentLocation = curLatLng;
// Returning closes result means we use the controller for nearby card. So no need to set search this area flags
if (!returnClosestResult) {
// To remember latest search either around user or any point on map
latestSearchLocation = searchLatLng;
latestSearchRadius = nearbyPlaces.radius*1000; // to meter
// Our radius searched around us, will be used to understand when user search their own location, we will follow them
if (checkingAroundCurrentLocation) {
currentLocationSearchRadius = nearbyPlaces.radius*1000; // to meter
currentLocation = curLatLng;
}
}
return nearbyPlacesInfo;
}
else {
@ -112,7 +126,7 @@ public class NearbyController {
* @param placeList list of nearby places in Place data type
* @return Place list that holds nearby places
*/
static List<Place> loadAttractionsFromLocationToPlaces(
public static List<Place> loadAttractionsFromLocationToPlaces(
LatLng curLatLng,
List<Place> placeList) {
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));
@ -212,5 +226,7 @@ public class NearbyController {
public class NearbyPlacesInfo {
public List<Place> placeList; // List of nearby places
public LatLng[] boundaryCoordinates; // Corners of nearby area
public LatLng curLatLng; // Current location when this places are populated
public LatLng searchLatLng; // Search location for finding this places
}
}

View file

@ -1,723 +0,0 @@
package fr.free.nrw.commons.nearby;
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
import com.google.gson.Gson;
import java.util.List;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.utils.FragmentUtils;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.contributions.MainActivity.NEARBY_TAB_POSITION;
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.location.LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED;
public class NearbyFragment extends CommonsDaggerSupportFragment
implements LocationUpdateListener,
WikidataEditListener.WikidataP18EditListener {
@BindView(R.id.progressBar)
ProgressBar progressBar;
@BindView(R.id.bottom_sheet)
LinearLayout bottomSheet;
@BindView(R.id.bottom_sheet_details)
LinearLayout bottomSheetDetails;
@BindView(R.id.transparentView)
View transparentView;
@BindView(R.id.container_sheet)
FrameLayout frameLayout;
@BindView(R.id.loading_nearby_list)
ConstraintLayout loading_nearby_layout;
@Inject
LocationServiceManager locationManager;
@Inject
NearbyController nearbyController;
@Inject
WikidataEditListener wikidataEditListener;
@Inject Gson gson;
public NearbyMapFragment nearbyMapFragment;
private NearbyListFragment nearbyListFragment;
private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName();
private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName();
private Bundle bundle;
private BottomSheetBehavior bottomSheetBehavior; // Behavior for list bottom sheet
private BottomSheetBehavior bottomSheetBehaviorForDetails; // Behavior for details bottom sheet
private LatLng curLatLng;
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
public View view;
private Snackbar snackbar;
private LatLng lastKnownLocation;
private LatLng customLatLng;
private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
private BroadcastReceiver broadcastReceiver;
private boolean onOrientationChanged = false;
private boolean populateForCurrentLocation = false;
private boolean isNetworkErrorOccured = false;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_nearby, container, false);
ButterKnife.bind(this, view);
/*// Resume the fragment if exist
resumeFragment();*/
bundle = new Bundle();
initBottomSheetBehaviour();
this.view = view;
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState != null) {
onOrientationChanged = true;
}
}
/**
* Hide or expand bottom sheet according to states of all sheets
*/
public void listOptionMenuItemClicked() {
if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_COLLAPSED || bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_HIDDEN){
bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}else if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_EXPANDED){
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
}
/**
* Resume fragments if they exists
*/
private void resumeFragment() {
// Find the retained fragment on activity restarts
nearbyMapFragment = getMapFragment();
nearbyListFragment = getListFragment();
addNetworkBroadcastReceiver();
}
/**
* Returns the map fragment added to child fragment manager previously, if exists.
*/
private NearbyMapFragment getMapFragment() {
return (NearbyMapFragment) getChildFragmentManager().findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT);
}
private void removeMapFragment() {
if (nearbyMapFragment != null) {
FragmentManager fm = getFragmentManager();
fm.beginTransaction().remove(nearbyMapFragment).commit();
nearbyMapFragment = null;
}
}
/**
* Returns the list fragment added to child fragment manager previously, if exists.
*/
private NearbyListFragment getListFragment() {
return (NearbyListFragment) getChildFragmentManager().findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT);
}
private void removeListFragment() {
if (nearbyListFragment != null) {
FragmentManager fm = getFragmentManager();
fm.beginTransaction().remove(nearbyListFragment).commit();
nearbyListFragment = null;
}
}
/**
* Initialize bottom sheet behaviour (sheet for map list.) Set height 9/16 of all window.
* Add callback for bottom sheet changes, so that we can sync it with bottom sheet for details
* (sheet for nearby details)
*/
private void initBottomSheetBehaviour() {
transparentView.setAlpha(0);
bottomSheet.getLayoutParams().height = getActivity().getWindowManager()
.getDefaultDisplay().getHeight() / 16 * 9;
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(View bottomSheet, int unusedNewState) {
prepareViewsForSheetPosition();
}
@Override
public void onSlide(View bottomSheet, float slideOffset) {
}
});
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetBehaviorForDetails = BottomSheetBehavior.from(bottomSheetDetails);
bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN);
}
/**
* Sets camera position, zoom level according to sheet positions
*/
private void prepareViewsForSheetPosition() {
// TODO
}
@Override
public void onLocationChangedSignificantly(LatLng latLng) {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
@Override
public void onLocationChangedSlightly(LatLng latLng) {
refreshView(LOCATION_SLIGHTLY_CHANGED);
}
@Override
public void onLocationChangedMedium(LatLng latLng) {
// For nearby map actions, there are no differences between 500 meter location change (aka medium change) and slight change
refreshView(LOCATION_SLIGHTLY_CHANGED);
}
@Override
public void onWikidataEditSuccessful() {
// Do not refresh nearby map if we are checking other areas with search this area button
if (nearbyMapFragment != null && !nearbyMapFragment.searchThisAreaModeOn) {
refreshView(MAP_UPDATED);
}
}
/**
* This method should be the single point to load/refresh nearby places
*
* @param locationChangeType defines if location changed significantly or slightly
*/
private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) {
Timber.d("Refreshing nearby places");
if (lockNearbyView) {
return;
}
if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) {
hideProgressBar();
return;
}
registerLocationUpdates();
LatLng lastLocation = locationManager.getLastLocation();
if (curLatLng != null && curLatLng.equals(lastLocation)
&& !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed
// Two exceptional cases to refresh nearby map manually.
if (!onOrientationChanged) {
return;
}
}
curLatLng = lastLocation;
if (locationChangeType.equals(PERMISSION_JUST_GRANTED)) {
curLatLng = lastKnownLocation;
}
if (curLatLng == null) {
Timber.d("Skipping update of nearby places as location is unavailable");
return;
}
/*
onOrientation changed is true whenever activities orientation changes. After orientation
change we want to refresh map significantly, doesn't matter if location changed significantly
or not. Thus, we included onOrientationChanged boolean to if clause
*/
if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)
|| locationChangeType.equals(PERMISSION_JUST_GRANTED)
|| locationChangeType.equals(MAP_UPDATED)
|| onOrientationChanged) {
progressBar.setVisibility(View.VISIBLE);
//TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found
String gsonCurLatLng = gson.toJson(curLatLng);
bundle.clear();
bundle.putString("CurLatLng", gsonCurLatLng);
compositeDisposable.add(Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng, curLatLng, false, true))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces,
throwable -> {
Timber.d(throwable);
showErrorMessage(getString(R.string.error_fetching_nearby_places));
progressBar.setVisibility(View.GONE);
}));
} else if (locationChangeType
.equals(LOCATION_SLIGHTLY_CHANGED) && nearbyMapFragment != null) {
String gsonCurLatLng = gson.toJson(curLatLng);
bundle.putString("CurLatLng", gsonCurLatLng);
updateMapFragment(false,true, null, null);
}
if (nearbyMapFragment != null && nearbyMapFragment.searchThisAreaButton != null) {
nearbyMapFragment.searchThisAreaButton.setVisibility(View.GONE);
}
}
/**
* This method should be used with "Search this are button". This method will search nearby
* points around any custom location (target location when user clicked on search this area)
* button. It populates places for custom location.
* @param customLatLng Custom area which we will search around
*/
void refreshViewForCustomLocation(LatLng customLatLng, boolean refreshForCurrentLocation) {
if (customLatLng == null) {
// If null, return
return;
}
populateForCurrentLocation = refreshForCurrentLocation;
this.customLatLng = customLatLng;
compositeDisposable.add(Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng, customLatLng, false, populateForCurrentLocation))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlacesFromCustomLocation,
throwable -> {
Timber.d(throwable);
showErrorMessage(getString(R.string.error_fetching_nearby_places));
}));
if (nearbyMapFragment != null) {
nearbyMapFragment.searchThisAreaButton.setVisibility(View.GONE);
}
}
/**
* Populates places for custom location, should be used for finding nearby places around a
* location where you are not at.
* @param nearbyPlacesInfo This variable has place list information and distances.
*/
private void populatePlacesFromCustomLocation(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
if (nearbyMapFragment != null) {
nearbyMapFragment.searchThisAreaButtonProgressBar.setVisibility(View.GONE);
}
if (nearbyMapFragment != null && curLatLng != null) {
if (!populateForCurrentLocation) {
nearbyMapFragment.updateMapSignificantlyForCustomLocation(customLatLng, nearbyPlacesInfo.placeList);
} else {
updateMapFragment(true,true, customLatLng, nearbyPlacesInfo);
}
updateListFragmentForCustomLocation(nearbyPlacesInfo.placeList);
}
}
/**
* Turns nearby place lists and boundary coordinates into gson and update map and list fragments
* accordingly
* @param nearbyPlacesInfo a variable holds both nearby place list and boundary coordinates
*/
private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
Timber.d("Populating nearby places");
List<Place> placeList = nearbyPlacesInfo.placeList;
LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates;
String gsonPlaceList = gson.toJson(placeList);
String gsonCurLatLng = gson.toJson(curLatLng);
String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates);
if (placeList.size() == 0) {
ViewUtil.showShortSnackbar(view.findViewById(R.id.container), R.string.no_nearby);
}
bundle.putString("PlaceList", gsonPlaceList);
bundle.putString("BoundaryCoord", gsonBoundaryCoordinates);
// First time to init fragments
if (nearbyMapFragment == null) {
Timber.d("Init map fragment for the first time");
lockNearbyView(true);
setMapFragment();
setListFragment();
hideProgressBar();
lockNearbyView(false);
} else {
// There are fragments, just update the map and list
Timber.d("Map fragment already exists, just update the map and list");
updateMapFragment(false,false, null, null);
updateListFragment();
}
}
/**
* Lock nearby view updates while updating map or list. Because we don't want new update calls
* when we already updating for old location update.
* @param lock true if we should lock nearby map
*/
private void lockNearbyView(boolean lock) {
if (lock) {
lockNearbyView = true;
locationManager.unregisterLocationManager();
locationManager.removeLocationListener(this);
} else {
lockNearbyView = false;
registerLocationUpdates();
locationManager.addLocationListener(this);
}
}
/**
* Updates map fragment,
* For slight update: camera follows users location
* For significant update: nearby markers are removed and new markers added again
* Slight updates stop if user is checking another area of map
*
* @param updateViaButton search this area button is clicked
* @param isSlightUpdate Means no need to update markers, just follow user location with camera
* @param customLatLng Will be used for updates for other locations than users current location.
* Ie. when we use search this area feature
* @param nearbyPlacesInfo Includes nearby places list and boundary coordinates
*/
private void updateMapFragment(boolean updateViaButton, boolean isSlightUpdate, @Nullable LatLng customLatLng, @Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
/*
Significant update means updating nearby place markers. Slightly update means only
updating current location marker and camera target.
We update our map Significantly on each 1000 meter change, but we can't never know
the frequency of nearby places. Thus we check if we are close to the boundaries of
our nearby markers, we update our map Significantly.
*/
NearbyMapFragment nearbyMapFragment = getMapFragment();
if (nearbyMapFragment != null && !nearbyMapFragment.isCurrentLocationMarkerVisible() && !onOrientationChanged) {
Timber.d("Do not update the map, user is not seeing current location marker" +
" means they are checking around and moving on map");
return;
}
if (nearbyMapFragment != null && curLatLng != null) {
hideProgressBar(); // In case it is visible (this happens, not an impossible case)
/*
* If we are close to nearby places boundaries, we need a significant update to
* get new nearby places. Check order is south, north, west, east
* */
if (nearbyMapFragment.boundaryCoordinates != null
&& !nearbyMapFragment.checkingAround
&& !nearbyMapFragment.searchThisAreaModeOn
&& !onOrientationChanged
&& (curLatLng.getLatitude() < nearbyMapFragment.boundaryCoordinates[0].getLatitude()
|| curLatLng.getLatitude() > nearbyMapFragment.boundaryCoordinates[1].getLatitude()
|| curLatLng.getLongitude() < nearbyMapFragment.boundaryCoordinates[2].getLongitude()
|| curLatLng.getLongitude() > nearbyMapFragment.boundaryCoordinates[3].getLongitude())) {
// populate places
compositeDisposable.add(Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng, curLatLng, false, updateViaButton))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces,
throwable -> {
Timber.d(throwable);
showErrorMessage(getString(R.string.error_fetching_nearby_places));
progressBar.setVisibility(View.GONE);
}));
nearbyMapFragment.setBundleForUpdates(bundle);
nearbyMapFragment.updateMapSignificantlyForCurrentLocation();
updateListFragment();
return;
}
if (updateViaButton) {
nearbyMapFragment.updateMapSignificantlyForCustomLocation(customLatLng, nearbyPlacesInfo.placeList);
return;
}
/*
If this is the map update just after orientation change, then it is not a slight update
anymore. We want to significantly update map after each orientation change
*/
if (onOrientationChanged) {
isSlightUpdate = false;
onOrientationChanged = false;
}
if (isSlightUpdate) {
nearbyMapFragment.setBundleForUpdates(bundle);
nearbyMapFragment.updateMapSlightly();
} else {
nearbyMapFragment.setBundleForUpdates(bundle);
nearbyMapFragment.updateMapSignificantlyForCurrentLocation();
updateListFragment();
}
} else {
lockNearbyView(true);
setMapFragment();
setListFragment();
hideProgressBar();
lockNearbyView(false);
}
}
/**
* Updates already existing list fragment with bundle includes nearby places and boundary
* coordinates
*/
private void updateListFragment() {
nearbyListFragment.setBundleForUpdates(bundle);
nearbyListFragment.updateNearbyListSignificantly();
}
/**
* Updates nearby list for custom location, will be used with search this area method. When you
* want to search for a place where you are not at.
* @param placeList List of places around your manually chosen target location from map.
*/
private void updateListFragmentForCustomLocation(List<Place> placeList) {
nearbyListFragment.updateNearbyListSignificantlyForCustomLocation(placeList);
}
/**
* Calls fragment for map view.
*/
private void setMapFragment() {
FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction();
nearbyMapFragment = new NearbyMapFragment();
nearbyMapFragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container, nearbyMapFragment, TAG_RETAINED_MAP_FRAGMENT);
fragmentTransaction.commitAllowingStateLoss();
}
/**
* Calls fragment for list view.
*/
private void setListFragment() {
loading_nearby_layout.setVisibility(View.GONE);
frameLayout.setVisibility(View.VISIBLE);
FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction();
nearbyListFragment = new NearbyListFragment();
nearbyListFragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container_sheet, nearbyListFragment, TAG_RETAINED_LIST_FRAGMENT);
initBottomSheetBehaviour();
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
fragmentTransaction.commitAllowingStateLoss();
}
/**
* Hides progress bar
*/
private void hideProgressBar() {
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
}
}
/**
* This method first checks if the location permissions has been granted and then register the location manager for updates.
*/
private void registerLocationUpdates() {
locationManager.registerLocationManager();
}
private void showErrorMessage(String message) {
ViewUtil.showLongToast(getActivity(), message);
}
/**
* Adds network broadcast receiver to recognize connection established
*/
private void addNetworkBroadcastReceiver() {
if (!FragmentUtils.isFragmentUIActive(this)) {
return;
}
if (broadcastReceiver != null) {
return;
}
IntentFilter intentFilter = new IntentFilter(NETWORK_INTENT_ACTION);
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (getActivity() != null) {
if (NetworkUtils.isInternetConnectionEstablished(getActivity())) {
if (isNetworkErrorOccured) {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
isNetworkErrorOccured = false;
}
if (snackbar != null) {
snackbar.dismiss();
snackbar = null;
}
} else {
if (snackbar == null) {
snackbar = Snackbar.make(view, R.string.no_internet, Snackbar.LENGTH_INDEFINITE);
if (nearbyMapFragment != null && nearbyMapFragment.searchThisAreaButton != null) {
nearbyMapFragment.searchThisAreaButton.setVisibility(View.GONE);
}
}
isNetworkErrorOccured = true;
snackbar.show();
}
}
}
};
getActivity().registerReceiver(broadcastReceiver, intentFilter);
}
@Override
public void onResume() {
super.onResume();
// Resume the fragment if exist
if (((MainActivity) getActivity()).viewPager.getCurrentItem() == NEARBY_TAB_POSITION) {
checkPermissionsAndPerformAction(this::resumeFragment);
} else {
resumeFragment();
}
}
/**
* Perform nearby operations on nearby tab selected
* @param onOrientationChanged pass orientation changed info to fragment
*/
public void onTabSelected(boolean onOrientationChanged) {
Timber.d("On nearby tab selected");
this.onOrientationChanged = onOrientationChanged;
checkPermissionsAndPerformAction(this::performNearbyOperations);
}
private void checkPermissionsAndPerformAction(Runnable runnable) {
PermissionUtils.checkPermissionsAndPerformAction(getActivity(),
Manifest.permission.ACCESS_FINE_LOCATION,
runnable,
() -> ((MainActivity) getActivity()).viewPager.setCurrentItem(CONTRIBUTIONS_TAB_POSITION),
R.string.location_permission_title,
R.string.location_permission_rationale_nearby);
}
/**
* Calls nearby operations in required order.
*/
private void performNearbyOperations() {
locationManager.addLocationListener(this);
registerLocationUpdates();
lockNearbyView = false;
addNetworkBroadcastReceiver();
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
wikidataEditListener.setAuthenticationStateListener(this);
}
@Override
public void onDestroy() {
super.onDestroy();
wikidataEditListener.setAuthenticationStateListener(null);
}
@Override
public void onDetach() {
super.onDetach();
snackbar = null;
broadcastReceiver = null;
wikidataEditListener.setAuthenticationStateListener(null);
}
@Override
public void onPause() {
super.onPause();
// this means that this activity will not be recreated now, user is leaving it
// or the activity is otherwise finishing
if(getActivity().isFinishing()) {
// we will not need this fragment anymore, this may also be a good place to signal
// to the retained fragment object to perform its own cleanup.
//removeMapFragment();
removeListFragment();
}
if (broadcastReceiver != null) {
getActivity().unregisterReceiver(broadcastReceiver);
broadcastReceiver = null;
}
if (locationManager != null) {
locationManager.removeLocationListener(this);
locationManager.unregisterLocationManager();
}
}
/**
* Centers the map in nearby fragment to a given place
* @param place is new center of the map
*/
public void centerMapToPlace(Place place) {
if (nearbyMapFragment != null) {
nearbyMapFragment.centerMapToPlace(place);
}
}
public boolean isBottomSheetExpanded() { return bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED;
}
}

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.nearby;
import com.mapbox.mapboxsdk.annotations.Marker;
class NearbyMarker extends Marker {
public class NearbyMarker extends Marker {
private final Place place;
private NearbyBaseMarker nearbyBaseMarker;
@ -17,7 +17,7 @@ class NearbyMarker extends Marker {
this.nearbyBaseMarker = baseMarkerOptions;
}
NearbyBaseMarker getNearbyBaseMarker() {
public NearbyBaseMarker getNearbyBaseMarker() {
return nearbyBaseMarker;
}

View file

@ -13,6 +13,7 @@ import android.widget.TextView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
import fr.free.nrw.commons.utils.SwipableCardView;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
@ -90,7 +91,7 @@ public class NearbyNotificationCardView extends SwipableCardView {
m.viewPager.setCurrentItem(NEARBY_TAB_POSITION);
// Center the map to the place
((NearbyFragment) m.contributionsActivityPagerAdapter.getItem(NEARBY_TAB_POSITION)).centerMapToPlace(place);
((NearbyParentFragment) m.contributionsActivityPagerAdapter.getItem(NEARBY_TAB_POSITION)).centerMapToPlace(place);
});
}

View file

@ -123,7 +123,7 @@ public class Place implements Parcelable {
* Checks if the Wikidata item has a Wikipedia page associated with it
* @return true if there is a Wikipedia link
*/
boolean hasWikipediaLink() {
public boolean hasWikipediaLink() {
return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikipediaLink()));
}
@ -131,7 +131,7 @@ public class Place implements Parcelable {
* Checks if the Wikidata item has a Wikidata page associated with it
* @return true if there is a Wikidata link
*/
boolean hasWikidataLink() {
public boolean hasWikidataLink() {
return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikidataLink()));
}
@ -139,7 +139,7 @@ public class Place implements Parcelable {
* Checks if the Wikidata item has a Commons page associated with it
* @return true if there is a Commons link
*/
boolean hasCommonsLink() {
public boolean hasCommonsLink() {
return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getCommonsLink()));
}

View file

@ -33,8 +33,11 @@ import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.nearby.fragments.NearbyMapFragment;
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
import timber.log.Timber;
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.TAG_RETAINED_MAP_FRAGMENT;
import static fr.free.nrw.commons.theme.NavigationBaseActivity.startActivityWithFlags;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
@ -117,7 +120,7 @@ public class PlaceRenderer extends Renderer<Place> {
}
}
if (onBookmarkClick == null) {
((NearbyFragment) fragment.getParentFragment()).centerMapToPlace(place);
((NearbyParentFragment) fragment.getParentFragment()).centerMapToPlace(place);
}
};
view.setOnClickListener(listener);
@ -190,7 +193,9 @@ public class PlaceRenderer extends Renderer<Place> {
onBookmarkClick.onClick();
}
else {
((NearbyMapFragment)((NearbyFragment)((NearbyListFragment)fragment).getParentFragment()).getChildFragmentManager().findFragmentByTag(NearbyMapFragment.class.getSimpleName())).updateMarker(isBookmarked, place);
((NearbyMapFragment)(fragment.getParentFragment()).getChildFragmentManager().
findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT)).
updateMarker(isBookmarked, place, null);
}
}
});

View file

@ -0,0 +1,31 @@
package fr.free.nrw.commons.nearby.contract;
import com.mapbox.mapboxsdk.annotations.Marker;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import java.util.List;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.NearbyBaseMarker;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter;
/**
* This interface defines specific View and UserActions for map
* part of the nearby.
*/
public interface NearbyMapContract {
interface View{
void updateMapMarkers(LatLng latLng, List<Place> placeList, Marker selectedMarker, NearbyParentFragmentPresenter nearbyParentFragmentPresenter);
void updateMapToTrackPosition(LatLng curLatLng);
void addCurrentLocationMarker(LatLng curLatLng);
void addNearbyMarkersToMapBoxMap(List<NearbyBaseMarker> baseMarkerOptions, Marker marker, NearbyParentFragmentPresenter nearbyParentFragmentPresenter);
LatLng getCameraTarget();
MapboxMap getMapboxMap();
void viewsAreAssignedToPresenter(NearbyParentFragmentContract.ViewsAreReadyCallback viewsAreReadyCallback);
void addOnCameraMoveListener(MapboxMap.OnCameraMoveListener onCameraMoveListener);
void centerMapToPlace(Place place, boolean isPortraitMode);
void removeCurrentLocationMarker();
}
}

View file

@ -0,0 +1,57 @@
package fr.free.nrw.commons.nearby.contract;
import com.mapbox.mapboxsdk.annotations.Marker;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import java.util.List;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.nearby.Place;
public interface NearbyParentFragmentContract {
interface View {
void registerLocationUpdates(LocationServiceManager locationServiceManager);
boolean isNetworkConnectionEstablished();
void addNetworkBroadcastReceiver();
void listOptionMenuItemClicked();
void populatePlaces(LatLng curlatLng, LatLng searchLatLng);
boolean isListBottomSheetExpanded();
void checkPermissionsAndPerformAction(Runnable runnable);
void displayLoginSkippedWarning();
void setFABPlusAction(android.view.View.OnClickListener onClickListener);
void setFABRecenterAction(android.view.View.OnClickListener onClickListener);
void animateFABs();
void recenterMap(LatLng curLatLng);
void hideBottomSheet();
void displayBottomSheetWithInfo(Marker marker);
void addOnCameraMoveListener(MapboxMap.OnCameraMoveListener onCameraMoveListener);
void addSearchThisAreaButtonAction();
void setSearchThisAreaButtonVisibility(boolean isVisible);
void setProgressBarVisibility(boolean isVisible);
void setTabItemContributions();
boolean isDetailsBottomSheetVisible();
void setBottomSheetDetailsSmaller();
boolean isSearchThisAreaButtonVisible();
}
interface NearbyListView {
void updateListFragment(List<Place> placeList);
}
interface UserActions {
void onTabSelected();
void checkForPermission();
void updateMapAndList(LocationServiceManager.LocationChangeType locationChangeType, LatLng cameraTarget);
void lockUnlockNearby(boolean isNearbyLocked);
void setActionListeners(JsonKvStore applicationKvStore);
void backButtonClicked();
MapboxMap.OnCameraMoveListener onCameraMove(MapboxMap mapboxMap);
}
interface ViewsAreReadyCallback {
void nearbyFragmentsAreReady();
}
}

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.nearby;
package fr.free.nrw.commons.nearby.fragments;
import android.content.Context;
import android.os.Bundle;
@ -22,10 +22,13 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.NearbyAdapterFactory;
import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
import timber.log.Timber;
public class NearbyListFragment extends CommonsDaggerSupportFragment {
private Bundle bundleForUpdates; // Carry information from activity about changed nearby places and current location
public class NearbyListFragment extends CommonsDaggerSupportFragment implements NearbyParentFragmentContract.NearbyListView {
private static final Type LIST_TYPE = new TypeToken<List<Place>>() {
}.getType();
@ -69,30 +72,6 @@ public class NearbyListFragment extends CommonsDaggerSupportFragment {
recyclerView.setAdapter(adapterFactory.create(getPlaceListFromBundle(bundle)));
}
/**
* Updates nearby list elements all together
*/
public void updateNearbyListSignificantly() {
try {
adapterFactory.updateAdapterData(getPlaceListFromBundle(bundleForUpdates), (RVRendererAdapter<Place>) recyclerView.getAdapter());
} catch (NullPointerException e) {
Timber.e("Null pointer exception from calling recyclerView.getAdapter()");
}
}
/**
* While nearby updates for current location held with bundle, automatically, custom updates are
* done by calling this method, triggered by search this are button input from user.
* @param placeList List of nearby places to be added list fragment
*/
public void updateNearbyListSignificantlyForCustomLocation(List<Place> placeList) {
try {
adapterFactory.updateAdapterData(placeList, (RVRendererAdapter<Place>) recyclerView.getAdapter());
} catch (NullPointerException e) {
Timber.e("Null pointer exception from calling recyclerView.getAdapter()");
}
}
/**
* When user moved too much, we need to update nearby list too. This operation is made by passing
* a bundle from NearbyFragment to NearbyListFragment and NearbyMapFragment. This method extracts
@ -116,12 +95,9 @@ public class NearbyListFragment extends CommonsDaggerSupportFragment {
return placeList;
}
/**
* Sets bundles for updates in map. Ie. user is moved too much so we need to update nearby markers.
* @param bundleForUpdates includes new calculated nearby places.
*/
public void setBundleForUpdates(Bundle bundleForUpdates) {
this.bundleForUpdates = bundleForUpdates;
@Override
public void updateListFragment(List<Place> placeList) {
Timber.d("Update list fragment");
adapterFactory.updateAdapterData(placeList, (RVRendererAdapter<Place>) recyclerView.getAdapter());
}
}

View file

@ -0,0 +1,457 @@
package fr.free.nrw.commons.nearby.fragments;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import com.mapbox.mapboxsdk.annotations.Icon;
import com.mapbox.mapboxsdk.annotations.IconFactory;
import com.mapbox.mapboxsdk.annotations.Marker;
import com.mapbox.mapboxsdk.annotations.MarkerOptions;
import com.mapbox.mapboxsdk.annotations.Polygon;
import com.mapbox.mapboxsdk.annotations.PolygonOptions;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.maps.MapFragment;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.utils.MapFragmentUtils;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.NearbyBaseMarker;
import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.nearby.NearbyMarker;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.contract.NearbyMapContract;
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter;
import fr.free.nrw.commons.utils.LocationUtils;
import fr.free.nrw.commons.utils.UiUtils;
import timber.log.Timber;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
/**
* Support Fragment wrapper around a map view.
* <p>
* A Map component in an app. This fragment is the simplest way to place a map in an application.
* It's a wrapper around a view of a map to automatically handle the necessary life cycle needs.
* Being a fragment, this component can be added to an activity's layout or can dynamically be added
* using a FragmentManager.
* </p>
* <p>
* To get a reference to the MapView, use {@link #getMapAsync(OnMapReadyCallback)}}
* </p>
*
* @see #getMapAsync(OnMapReadyCallback)
*/
public class NearbyMapFragment extends CommonsDaggerSupportFragment
implements OnMapReadyCallback, NearbyMapContract.View{
@Inject
BookmarkLocationsDao bookmarkLocationDao;
private final List<OnMapReadyCallback> mapReadyCallbackList = new ArrayList<>();
private MapFragment.OnMapViewReadyCallback mapViewReadyCallback;
private MapboxMap mapboxMap;
private MapView map;
private Marker currentLocationMarker;
private Polygon currentLocationPolygon;
private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.005;
private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.004;
private static final double ZOOM_LEVEL = 14f;
/**
* Creates a default MapFragment instance
*
* @return MapFragment created
*/
public static NearbyMapFragment newInstance() {
return new NearbyMapFragment();
}
/**
* Creates a MapFragment instance
*
* @param mapboxMapOptions The configuration options to be used.
* @return MapFragment created.
*/
@NonNull
public static NearbyMapFragment newInstance(@Nullable MapboxMapOptions mapboxMapOptions) {
NearbyMapFragment mapFragment = new NearbyMapFragment();
mapFragment.setArguments(MapFragmentUtils.createFragmentArgs(mapboxMapOptions));
return mapFragment;
}
/**
* Called when the context attaches to this fragment.
*
* @param context the context attaching
*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof MapFragment.OnMapViewReadyCallback) {
mapViewReadyCallback = (MapFragment.OnMapViewReadyCallback) context;
}
}
/**
* Called when this fragment is inflated, parses XML tag attributes.
*
* @param context The context inflating this fragment.
* @param attrs The XML tag attributes.
* @param savedInstanceState The saved instance state for the map fragment.
*/
@Override
public void onInflate(@NonNull Context context, AttributeSet attrs, Bundle savedInstanceState) {
super.onInflate(context, attrs, savedInstanceState);
setArguments(MapFragmentUtils.createFragmentArgs(MapboxMapOptions.createFromAttributes(context, attrs)));
}
/**
* Creates the fragment view hierarchy.
*
* @param inflater Inflater used to inflate content.
* @param container The parent layout for the map fragment.
* @param savedInstanceState The saved instance state for the map fragment.
* @return The view created
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
Context context = inflater.getContext();
map = new MapView(context, MapFragmentUtils.resolveArgs(context, getArguments()));
return map;
}
/**
* Called when the fragment view hierarchy is created.
*
* @param view The content view of the fragment
* @param savedInstanceState THe saved instance state of the framgnt
*/
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
map.onCreate(savedInstanceState);
map.getMapAsync(this);
// notify listeners about MapView creation
if (mapViewReadyCallback != null) {
mapViewReadyCallback.onMapViewReady(map);
}
}
@Override
public void onMapReady(@NonNull MapboxMap mapboxMap) {
this.mapboxMap = mapboxMap;
for (OnMapReadyCallback onMapReadyCallback : mapReadyCallbackList) {
onMapReadyCallback.onMapReady(mapboxMap);
}
}
/**
* Called when the fragment is visible for the users.
*/
@Override
public void onStart() {
super.onStart();
map.onStart();
}
/**
* Called when the fragment is ready to be interacted with.
*/
@Override
public void onResume() {
super.onResume();
map.onResume();
}
/**
* Called when the fragment is pausing.
*/
@Override
public void onPause() {
super.onPause();
map.onPause();
}
/**
* Called when the fragment state needs to be saved.
*
* @param outState The saved state
*/
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (map != null) {
map.onSaveInstanceState(outState);
}
}
/**
* Called when the fragment is no longer visible for the user.
*/
@Override
public void onStop() {
super.onStop();
map.onStop();
}
/**
* Called when the fragment receives onLowMemory call from the hosting Activity.
*/
@Override
public void onLowMemory() {
super.onLowMemory();
if (map != null) {
map.onLowMemory();
}
}
/**
* Called when the fragment is view hierarchy is being destroyed.
*/
@Override
public void onDestroyView() {
super.onDestroyView();
map.onDestroy();
}
/**
* Called when the fragment is destroyed.
*/
@Override
public void onDestroy() {
super.onDestroy();
mapReadyCallbackList.clear();
}
/**
* Sets a callback object which will be triggered when the MapboxMap instance is ready to be used.
*
* @param onMapReadyCallback The callback to be invoked.
*/
public void getMapAsync(@NonNull final OnMapReadyCallback onMapReadyCallback) {
if (mapboxMap == null) {
mapReadyCallbackList.add(onMapReadyCallback);
} else {
onMapReadyCallback.onMapReady(mapboxMap);
}
}
/**
* Clears map, adds nearby markers to map
* @param latLng current location
* @param placeList all places will be displayed on the map
* @param selectedMarker clicked marker by user
* @param nearbyParentFragmentPresenter presenter
*/
@Override
public void updateMapMarkers(LatLng latLng, List<Place> placeList
, Marker selectedMarker
, NearbyParentFragmentPresenter nearbyParentFragmentPresenter) {
Timber.d("Updates map markers");
List<NearbyBaseMarker> customBaseMarkerOptions = NearbyController
.loadAttractionsFromLocationToBaseMarkerOptions(latLng, // Curlatlang will be used to calculate distances
placeList,
getActivity(),
bookmarkLocationDao.getAllBookmarksLocations());
mapboxMap.clear();
addNearbyMarkersToMapBoxMap(customBaseMarkerOptions, selectedMarker, nearbyParentFragmentPresenter);
// Re-enable mapbox gestures on custom location markers load
mapboxMap.getUiSettings().setAllGesturesEnabled(true);
}
/**
* Makes map camera follow users location with animation
* @param curLatLng current location of user
*/
@Override
public void updateMapToTrackPosition(LatLng curLatLng) {
Timber.d("Updates map camera to track user position");
CameraPosition cameraPosition = new CameraPosition.Builder().target
(LocationUtils.commonsLatLngToMapBoxLatLng(curLatLng)).build();
mapboxMap.setCameraPosition(cameraPosition);
mapboxMap.animateCamera(CameraUpdateFactory
.newCameraPosition(cameraPosition), 1000);
}
/**
* Adds a marker for the user's current position. Adds a
* circle which uses the accuracy * 2, to draw a circle
* which represents the user's position with an accuracy
* of 95%.
*
* Should be called only on creation of mapboxMap, there
* is other method to update markers location with users
* move.
* @param curLatLng current location
*/
@Override
public void addCurrentLocationMarker(LatLng curLatLng) {
Timber.d("Adds current location marker");
Icon icon = IconFactory.getInstance(getContext()).fromResource(R.drawable.current_location_marker);
MarkerOptions currentLocationMarkerOptions = new MarkerOptions()
.position(new com.mapbox.mapboxsdk.geometry.LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()));
currentLocationMarkerOptions.setIcon(icon); // Set custom icon
currentLocationMarker = mapboxMap.addMarker(currentLocationMarkerOptions);
List<com.mapbox.mapboxsdk.geometry.LatLng> circle = UiUtils.createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(),
curLatLng.getAccuracy() * 2, 100);
PolygonOptions currentLocationPolygonOptions = new PolygonOptions()
.addAll(circle)
.strokeColor(getResources().getColor(R.color.current_marker_stroke))
.fillColor(getResources().getColor(R.color.current_marker_fill));
currentLocationPolygon = mapboxMap.addPolygon(currentLocationPolygonOptions);
}
@Override
public void removeCurrentLocationMarker() {
mapboxMap.removeMarker(currentLocationMarker);
mapboxMap.removePolygon(currentLocationPolygon);
}
/**
* Adds markers to map
* @param baseMarkerList is markers will be added
* @param selectedMarker is selected marker by user
* @param nearbyParentFragmentPresenter presenter
*/
@Override
public void addNearbyMarkersToMapBoxMap(@Nullable List<NearbyBaseMarker> baseMarkerList
, Marker selectedMarker
, NearbyParentFragmentPresenter nearbyParentFragmentPresenter) {
mapboxMap.addMarkers(baseMarkerList);
map.getMapAsync(mapboxMap -> {
mapboxMap.addMarkers(baseMarkerList);
setMapMarkerActions(selectedMarker, nearbyParentFragmentPresenter);
});
}
@Override
public LatLng getCameraTarget() {
return LocationUtils
.mapBoxLatLngToCommonsLatLng(mapboxMap.getCameraPosition().target);
}
@Override
public MapboxMap getMapboxMap() {
return mapboxMap;
}
@Override
public void viewsAreAssignedToPresenter(NearbyParentFragmentContract.ViewsAreReadyCallback viewsAreReadyCallback) {
}
@Override
public void addOnCameraMoveListener(MapboxMap.OnCameraMoveListener onCameraMoveListener) {
}
void setMapMarkerActions(Marker selected, NearbyParentFragmentPresenter nearbyParentFragmentPresenter) {
getMapboxMap().setOnInfoWindowCloseListener(marker -> {
if (marker == selected) {
nearbyParentFragmentPresenter.markerUnselected();
}
});
getMapboxMap().setOnMarkerClickListener(marker -> {
if (marker instanceof NearbyMarker) {
nearbyParentFragmentPresenter.markerSelected(marker);
}
return false;
});
}
/**
* Sets marker icon according to marker status. Sets title and distance.
* @param isBookmarked true if place is bookmarked
* @param place
* @param curLatLng current location
*/
public void updateMarker(boolean isBookmarked, Place place, @Nullable LatLng curLatLng) {
VectorDrawableCompat vectorDrawable;
if (isBookmarked) {
vectorDrawable = VectorDrawableCompat.create(
getContext().getResources(), R.drawable.ic_custom_bookmark_marker, getContext().getTheme()
);
} else {
vectorDrawable = VectorDrawableCompat.create(
getContext().getResources(), R.drawable.ic_custom_map_marker, getContext().getTheme()
);
}
for (Marker marker : mapboxMap.getMarkers()) {
if (marker.getTitle() != null && marker.getTitle().equals(place.getName())) {
Bitmap icon = UiUtils.getBitmap(vectorDrawable);
if (curLatLng != null) {
String distance = formatDistanceBetween(curLatLng, place.location);
place.setDistance(distance);
}
NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
nearbyBaseMarker.title(place.name);
nearbyBaseMarker.position(
new com.mapbox.mapboxsdk.geometry.LatLng(
place.location.getLatitude(),
place.location.getLongitude()));
nearbyBaseMarker.place(place);
nearbyBaseMarker.icon(IconFactory.getInstance(getContext())
.fromBitmap(icon));
marker.setIcon(IconFactory.getInstance(getContext()).fromBitmap(icon));
}
}
}
/**
* Centers the map in nearby fragment to a given place
* @param place is new center of the map
*/
@Override
public void centerMapToPlace(Place place, boolean isPortraitMode) {
Timber.d("Map is centered to place");
double cameraShift;
if (isPortraitMode) {
cameraShift = CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT;
} else {
cameraShift = CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE;
}
CameraPosition position = new CameraPosition.Builder()
.target(LocationUtils.commonsLatLngToMapBoxLatLng(
new LatLng(place.location.getLatitude()-cameraShift,
place.getLocation().getLongitude(),
0))) // Sets the new camera position
.zoom(ZOOM_LEVEL) // Same zoom level
.build();
mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1000);
}
}

View file

@ -0,0 +1,884 @@
package fr.free.nrw.commons.nearby.fragments;
import android.Manifest;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.annotations.Marker;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
import com.mapbox.mapboxsdk.maps.Style;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.LoginActivity;
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.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.nearby.NearbyMarker;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter;
import fr.free.nrw.commons.utils.FragmentUtils;
import fr.free.nrw.commons.utils.NearbyFABUtils;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
public class NearbyParentFragment extends CommonsDaggerSupportFragment
implements NearbyParentFragmentContract.View,
WikidataEditListener.WikidataP18EditListener {
@BindView(R.id.bottom_sheet) View bottomSheetList;
@BindView(R.id.bottom_sheet_details) View bottomSheetDetails;
@BindView(R.id.transparentView) View transparentView;
@BindView(R.id.directionsButtonText) TextView directionsButtonText;
@BindView(R.id.wikipediaButtonText) TextView wikipediaButtonText;
@BindView(R.id.wikidataButtonText) TextView wikidataButtonText;
@BindView(R.id.commonsButtonText) TextView commonsButtonText;
@BindView(R.id.fab_plus) FloatingActionButton fabPlus;
@BindView(R.id.fab_camera) FloatingActionButton fabCamera;
@BindView(R.id.fab_gallery) FloatingActionButton fabGallery;
@BindView(R.id.fab_recenter) FloatingActionButton fabRecenter;
@BindView(R.id.bookmarkButtonImage) ImageView bookmarkButtonImage;
@BindView(R.id.bookmarkButton) LinearLayout bookmarkButton;
@BindView(R.id.wikipediaButton) LinearLayout wikipediaButton;
@BindView(R.id.wikidataButton) LinearLayout wikidataButton;
@BindView(R.id.directionsButton) LinearLayout directionsButton;
@BindView(R.id.commonsButton) LinearLayout commonsButton;
@BindView(R.id.description) TextView description;
@BindView(R.id.title) TextView title;
@BindView(R.id.category) TextView distance;
@BindView(R.id.icon) ImageView icon;
@BindView(R.id.search_this_area_button) Button searchThisAreaButton;
@BindView(R.id.map_progress_bar) ProgressBar progressBar;
@BindView(R.id.container_sheet) FrameLayout frameLayout;
@BindView(R.id.loading_nearby_list) ConstraintLayout loadingNearbyLayout;
@Inject LocationServiceManager locationManager;
@Inject NearbyController nearbyController;
@Inject @Named("default_preferences") JsonKvStore applicationKvStore;
@Inject BookmarkLocationsDao bookmarkLocationDao;
@Inject ContributionController controller;
@Inject WikidataEditListener wikidataEditListener;
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 double ZOOM_LEVEL = 14f;
private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
private BroadcastReceiver broadcastReceiver;
private boolean isNetworkErrorOccurred = false;
private Snackbar snackbar;
private View view;
private NearbyParentFragmentPresenter nearbyParentFragmentPresenter;
private boolean isDarkTheme;
private boolean isFABsExpanded;
private Marker selectedMarker;
private Place selectedPlace;
private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.005;
private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.004;
private NearbyMapFragment nearbyMapFragment;
private NearbyListFragment nearbyListFragment;
public static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName();
private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName();
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
view = inflater.inflate(R.layout.fragment_nearby_parent, container, false);
ButterKnife.bind(this, view);
// Inflate the layout for this fragment
return view;
}
@Override
public void onResume() {
super.onResume();
nearbyMapFragment = getNearbyMapFragment();
nearbyListFragment = getListFragment();
}
private void initViews() {
Timber.d("init views called");
initBottomSheets();
loadAnimations();
setBottomSheetCallbacks();
decideButtonVisibilities();
addActionToTitle();
}
/**
* Creates bottom sheet behaviours from bottom sheets, sets initial states and visibility
*/
private void initBottomSheets() {
bottomSheetListBehavior = BottomSheetBehavior.from(bottomSheetList);
bottomSheetDetailsBehavior = BottomSheetBehavior.from(bottomSheetDetails);
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetDetails.setVisibility(View.VISIBLE);
}
/**
* Defines how bottom sheets will act on click
*/
private void setBottomSheetCallbacks() {
bottomSheetDetailsBehavior.setBottomSheetCallback(new BottomSheetBehavior
.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
prepareViewsForSheetPosition(newState);
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
}
});
bottomSheetDetails.setOnClickListener(v -> {
if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
} else if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
});
bottomSheetList.getLayoutParams().height = getActivity().getWindowManager()
.getDefaultDisplay().getHeight() / 16 * 9;
bottomSheetListBehavior = BottomSheetBehavior.from(bottomSheetList);
bottomSheetListBehavior.setBottomSheetCallback(new BottomSheetBehavior
.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
}
});
}
/**
* Loads animations will be used for FABs
*/
private void loadAnimations() {
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
}
/**
* Fits buttons according to our layout
*/
private void decideButtonVisibilities() {
// Remove button text if they exceed 1 line or if internal layout has not been built
// Only need to check for directions button because it is the longest
if (directionsButtonText.getLineCount() > 1 || directionsButtonText.getLineCount() == 0) {
wikipediaButtonText.setVisibility(View.GONE);
wikidataButtonText.setVisibility(View.GONE);
commonsButtonText.setVisibility(View.GONE);
directionsButtonText.setVisibility(View.GONE);
}
}
/**
*
*/
private void addActionToTitle() {
title.setOnClickListener(view -> {
Utils.copy("place", title.getText().toString(), getContext());
Toast.makeText(getContext(), "Text copied to clipboard", Toast.LENGTH_SHORT).show();
if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
} else {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
});
}
/**
* Returns the list fragment added to child fragment manager previously, if exists.
*/
private NearbyListFragment getListFragment() {
NearbyListFragment existingFragment = (NearbyListFragment) getChildFragmentManager()
.findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT);
if (existingFragment == null) {
existingFragment = setListFragment();
}
return existingFragment;
}
/**
* Returns the map fragment added to child fragment manager previously, if exists.
*/
private NearbyMapFragment getNearbyMapFragment() {
NearbyMapFragment existingFragment = (NearbyMapFragment) getChildFragmentManager()
.findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT);
if (existingFragment == null) {
existingFragment = setMapFragment();
}
return existingFragment;
}
/**
* Creates the map fragment and prepares map
* @return the map fragment created
*/
private NearbyMapFragment setMapFragment() {
// Mapbox access token is configured here. This needs to be called either in your application
// object or in the same activity which contains the mapview.
Mapbox.getInstance(getActivity(), getString(R.string.mapbox_commons_app_token));
NearbyMapFragment mapFragment;
// Create fragment
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
// Build mapboxMap
isDarkTheme = applicationKvStore.getBoolean("theme", false);
MapboxMapOptions options = new MapboxMapOptions()
.compassGravity(Gravity.BOTTOM | Gravity.LEFT)
.compassMargins(new int[]{12, 0, 0, 24})
.logoEnabled(false)
.attributionEnabled(false)
.camera(new CameraPosition.Builder()
.zoom(ZOOM_LEVEL)
.target(new com.mapbox.mapboxsdk.geometry.LatLng(-52.6885, -70.1395))
.build());
// Create map fragment
mapFragment = NearbyMapFragment.newInstance(options);
// Add map fragment to parent container
getChildFragmentManager().executePendingTransactions();
transaction.add(R.id.container, mapFragment, TAG_RETAINED_MAP_FRAGMENT);
transaction.commit();
mapFragment.getMapAsync(mapboxMap ->
mapboxMap.setStyle(
NearbyParentFragment.this.isDarkTheme ? Style.DARK : Style.OUTDOORS, style -> {
NearbyParentFragment.this.childMapFragmentAttached();
// Map is set up and the style has loaded. Now you can add data or make other map adjustments
}));
return mapFragment;
}
/**
* Creates the list fragment and put it into container
* @return the list fragment created
*/
private NearbyListFragment setListFragment() {
NearbyListFragment nearbyListFragment;
loadingNearbyLayout.setVisibility(View.GONE);
frameLayout.setVisibility(View.VISIBLE);
FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction();
nearbyListFragment = new NearbyListFragment();
nearbyListFragment.setArguments(null);
fragmentTransaction.replace(R.id.container_sheet, nearbyListFragment, TAG_RETAINED_LIST_FRAGMENT);
fragmentTransaction.commitAllowingStateLoss();
return nearbyListFragment;
}
private void removeListFragment() {
if (nearbyListFragment != null) {
FragmentManager fm = getChildFragmentManager();
fm.beginTransaction().remove(nearbyListFragment).commit();
nearbyListFragment = null;
}
}
private void removeMapFragment() {
if (nearbyMapFragment != null) {
FragmentManager fm = getChildFragmentManager();
fm.beginTransaction().remove(nearbyMapFragment).commit();
nearbyMapFragment = null;
}
}
/**
* Calls presenter to center map to any place
* @param place the place we want to center map
*/
public void centerMapToPlace(Place place) {
if (nearbyMapFragment != null) {
nearbyParentFragmentPresenter.centerMapToPlace(place,
getActivity().getResources().getConfiguration().orientation ==
Configuration.ORIENTATION_PORTRAIT);
}
}
/**
* Thanks to this method we make sure NearbyMapFragment is ready and attached. So that we can
* prevent NPE caused by null child fragment. This method is called from child fragment when
* it is attached.
*/
private void childMapFragmentAttached() {
Timber.d("Child map fragment attached");
nearbyParentFragmentPresenter = NearbyParentFragmentPresenter.getInstance
(nearbyListFragment,this, nearbyMapFragment, locationManager);
nearbyParentFragmentPresenter.nearbyFragmentsAreReady();
initViews();
nearbyParentFragmentPresenter.setActionListeners(applicationKvStore);
}
@Override
public void addOnCameraMoveListener(MapboxMap.OnCameraMoveListener onCameraMoveListener) {
nearbyMapFragment.getMapboxMap().addOnCameraMoveListener(onCameraMoveListener);
}
@Override
public void registerLocationUpdates(LocationServiceManager locationManager) {
locationManager.registerLocationManager();
}
@Override
public boolean isNetworkConnectionEstablished() {
return NetworkUtils.isInternetConnectionEstablished(getActivity());
}
/**
* Adds network broadcast receiver to recognize connection established
*/
@Override
public void addNetworkBroadcastReceiver() {
if (!FragmentUtils.isFragmentUIActive(this)) {
return;
}
if (broadcastReceiver != null) {
return;
}
IntentFilter intentFilter = new IntentFilter(NETWORK_INTENT_ACTION);
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (getActivity() != null) {
if (NetworkUtils.isInternetConnectionEstablished(getActivity())) {
if (isNetworkErrorOccurred) {
nearbyParentFragmentPresenter.updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED, null);
isNetworkErrorOccurred = false;
}
if (snackbar != null) {
snackbar.dismiss();
snackbar = null;
}
} else {
if (snackbar == null) {
snackbar = Snackbar.make(view, R.string.no_internet, Snackbar.LENGTH_INDEFINITE);
setSearchThisAreaButtonVisibility(false);
setProgressBarVisibility(false);
}
isNetworkErrorOccurred = true;
snackbar.show();
}
}
}
};
getActivity().registerReceiver(broadcastReceiver, intentFilter);
}
/**
* Hide or expand bottom sheet according to states of all sheets
*/
@Override
public void listOptionMenuItemClicked() {
if(bottomSheetListBehavior.getState()== BottomSheetBehavior.STATE_COLLAPSED || bottomSheetListBehavior.getState()==BottomSheetBehavior.STATE_HIDDEN){
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}else if(bottomSheetListBehavior.getState()==BottomSheetBehavior.STATE_EXPANDED){
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
}
@Override
public void populatePlaces(fr.free.nrw.commons.location.LatLng curlatLng, fr.free.nrw.commons.location.LatLng searchLatLng) {
if (curlatLng.equals(searchLatLng)) { // Means we are checking around current location
populatePlacesForCurrentLocation(curlatLng, searchLatLng);
} else {
populatePlacesForAnotherLocation(curlatLng, searchLatLng);
}
}
private void populatePlacesForCurrentLocation(fr.free.nrw.commons.location.LatLng curlatLng,
fr.free.nrw.commons.location.LatLng searchLatLng) {
compositeDisposable.add(Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curlatLng, searchLatLng, false, true))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateMapMarkers,
throwable -> {
Timber.d(throwable);
// TODO: find out why NPE occurs here
// showErrorMessage(getString(R.string.error_fetching_nearby_places));
setProgressBarVisibility(false);
nearbyParentFragmentPresenter.lockUnlockNearby(false);
}));
}
private void populatePlacesForAnotherLocation(fr.free.nrw.commons.location.LatLng curlatLng,
fr.free.nrw.commons.location.LatLng searchLatLng) {
compositeDisposable.add(Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curlatLng, searchLatLng, false, false))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateMapMarkersForCustomLocation,
throwable -> {
Timber.d(throwable);
// TODO: find out why NPE occurs here
// showErrorMessage(getString(R.string.error_fetching_nearby_places));
setProgressBarVisibility(false);
nearbyParentFragmentPresenter.lockUnlockNearby(false);
}));
}
/**
* Populates places for your location, should be used for finding nearby places around a
* location where you are.
* @param nearbyPlacesInfo This variable has place list information and distances.
*/
private void updateMapMarkers(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
nearbyParentFragmentPresenter.updateMapMarkers(nearbyPlacesInfo, selectedMarker);
}
/**
* Populates places for custom location, should be used for finding nearby places around a
* location where you are not at.
* @param nearbyPlacesInfo This variable has place list information and distances.
*/
private void updateMapMarkersForCustomLocation(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
nearbyParentFragmentPresenter.updateMapMarkersForCustomLocation(nearbyPlacesInfo, selectedMarker);
}
@Override
public boolean isListBottomSheetExpanded() {
return bottomSheetListBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED;
}
@Override
public boolean isDetailsBottomSheetVisible() {
return !(bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN);
}
@Override
public void setBottomSheetDetailsSmaller() {
if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
}
@Override
public void addSearchThisAreaButtonAction() {
searchThisAreaButton.setOnClickListener(nearbyParentFragmentPresenter.onSearchThisAreaClicked());
}
@Override
public void setSearchThisAreaButtonVisibility(boolean isVisible) {
if (isVisible) {
searchThisAreaButton.setVisibility(View.VISIBLE);
} else {
searchThisAreaButton.setVisibility(View.GONE);
}
}
@Override
public boolean isSearchThisAreaButtonVisible() {
if (searchThisAreaButton.getVisibility() == View.VISIBLE) {
return true;
} else {
return false;
}
}
@Override
public void setProgressBarVisibility(boolean isVisible) {
if (isVisible) {
progressBar.setVisibility(View.VISIBLE);
} else {
progressBar.setVisibility(View.GONE);
}
}
@Override
public void setTabItemContributions() {
((MainActivity)getActivity()).viewPager.setCurrentItem(0);
}
@Override
public void checkPermissionsAndPerformAction(Runnable runnable) {
Timber.d("Checking permission and perfoming action");
PermissionUtils.checkPermissionsAndPerformAction(getActivity(),
Manifest.permission.ACCESS_FINE_LOCATION,
runnable,
() -> ((MainActivity) getActivity()).viewPager.setCurrentItem(CONTRIBUTIONS_TAB_POSITION),
R.string.location_permission_title,
R.string.location_permission_rationale_nearby);
}
/**
* Starts animation of fab plus (turning on opening) and other FABs
*/
@Override
public void animateFABs() {
if (fabPlus.isShown()){
if (isFABsExpanded) {
collapseFABs(isFABsExpanded);
} else {
expandFABs(isFABsExpanded);
}
}
}
private void showFABs() {
NearbyFABUtils.addAnchorToBigFABs(fabPlus, bottomSheetDetails.getId());
fabPlus.show();
NearbyFABUtils.addAnchorToSmallFABs(fabGallery, getView().findViewById(R.id.empty_view).getId());
NearbyFABUtils.addAnchorToSmallFABs(fabCamera, getView().findViewById(R.id.empty_view1).getId());
}
/**
* Expands camera and gallery FABs, turn forward plus FAB
* @param isFABsExpanded true if they are already expanded
*/
private void expandFABs(boolean isFABsExpanded){
if (!isFABsExpanded) {
showFABs();
fabPlus.startAnimation(rotate_forward);
fabCamera.startAnimation(fab_open);
fabGallery.startAnimation(fab_open);
fabCamera.show();
fabGallery.show();
this.isFABsExpanded = true;
}
}
/**
* Hides all fabs
*/
private void hideFABs() {
NearbyFABUtils.removeAnchorFromFAB(fabPlus);
fabPlus.hide();
NearbyFABUtils.removeAnchorFromFAB(fabCamera);
fabCamera.hide();
NearbyFABUtils.removeAnchorFromFAB(fabGallery);
fabGallery.hide();
}
/**
* Collapses camera and gallery FABs, turn back plus FAB
* @param isFABsExpanded
*/
private void collapseFABs(boolean isFABsExpanded){
if (isFABsExpanded) {
fabPlus.startAnimation(rotate_backward);
fabCamera.startAnimation(fab_close);
fabGallery.startAnimation(fab_close);
fabCamera.hide();
fabGallery.hide();
this.isFABsExpanded = false;
}
}
@Override
public void displayLoginSkippedWarning() {
if (applicationKvStore.getBoolean("login_skipped", false)) {
// prompt the user to login
new AlertDialog.Builder(getContext())
.setMessage(R.string.login_alert_message)
.setPositiveButton(R.string.login, (dialog, which) -> {
// logout of the app
BaseLogoutListener logoutListener = new BaseLogoutListener();
CommonsApplication app = (CommonsApplication) getActivity().getApplication();
app.clearApplicationData(getContext(), logoutListener);
})
.show();
}
}
/**
* onLogoutComplete is called after shared preferences and data stored in local database are cleared.
*/
private class BaseLogoutListener implements CommonsApplication.LogoutListener {
@Override
public void onLogoutComplete() {
Timber.d("Logout complete callback received.");
Intent nearbyIntent = new Intent( getActivity(), LoginActivity.class);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(nearbyIntent);
getActivity().finish();
}
}
@Override
public void setFABPlusAction(View.OnClickListener onClickListener) {
fabPlus.setOnClickListener(onClickListener);
}
@Override
public void setFABRecenterAction(View.OnClickListener onClickListener) {
fabRecenter.setOnClickListener(onClickListener);
}
@Override
public void recenterMap(fr.free.nrw.commons.location.LatLng curLatLng) {
nearbyMapFragment.removeCurrentLocationMarker();
nearbyMapFragment.addCurrentLocationMarker(curLatLng);
CameraPosition position;
if (ViewUtil.isPortrait(getActivity())){
position = new CameraPosition.Builder()
.target(isListBottomSheetExpanded() ?
new LatLng(curLatLng.getLatitude() - CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT,
curLatLng.getLongitude())
: new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude(), 0)) // Sets the new camera position
.zoom(isListBottomSheetExpanded() ?
ZOOM_LEVEL
: nearbyMapFragment.getMapboxMap().getCameraPosition().zoom) // Same zoom level
.build();
}else {
position = new CameraPosition.Builder()
.target(isListBottomSheetExpanded() ?
new LatLng(curLatLng.getLatitude() - CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE,
curLatLng.getLongitude())
: new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude(), 0)) // Sets the new camera position
.zoom(isListBottomSheetExpanded() ?
ZOOM_LEVEL
: nearbyMapFragment.getMapboxMap().getCameraPosition().zoom) // Same zoom level
.build();
}
nearbyMapFragment.getMapboxMap().animateCamera(CameraUpdateFactory.newCameraPosition(position), 1000);
}
@Override
public void hideBottomSheet() {
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
@Override
public void displayBottomSheetWithInfo(Marker marker) {
this.selectedMarker = marker;
NearbyMarker nearbyMarker = (NearbyMarker) marker;
Place place = nearbyMarker.getNearbyBaseMarker().getPlace();
passInfoToSheet(place);
hideBottomSheet();
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
/**
* If nearby details bottom sheet state is collapsed: show fab plus
* If nearby details bottom sheet state is expanded: show fab plus
* If nearby details bottom sheet state is hidden: hide all fabs
* @param bottomSheetState see bottom sheet states
*/
public void prepareViewsForSheetPosition(int bottomSheetState) {
switch (bottomSheetState) {
case (BottomSheetBehavior.STATE_COLLAPSED):
collapseFABs(isFABsExpanded);
if (!fabPlus.isShown()) showFABs();
this.getView().requestFocus();
break;
case (BottomSheetBehavior.STATE_EXPANDED):
this.getView().requestFocus();
break;
case (BottomSheetBehavior.STATE_HIDDEN):
nearbyMapFragment.getMapboxMap().deselectMarkers();
transparentView.setClickable(false);
transparentView.setAlpha(0);
collapseFABs(isFABsExpanded);
hideFABs();
if (this.getView() != null) {
this.getView().requestFocus();
}
break;
}
}
/**
* Same bottom sheet carries information for all nearby places, so we need to pass information
* (title, description, distance and links) to view on nearby marker click
* @param place Place of clicked nearby marker
*/
private void passInfoToSheet(Place place) {
this.selectedPlace = place;
updateBookmarkButtonImage(this.selectedPlace);
bookmarkButton.setOnClickListener(view -> {
boolean isBookmarked = bookmarkLocationDao.updateBookmarkLocation(this.selectedPlace);
updateBookmarkButtonImage(this.selectedPlace);
nearbyMapFragment.updateMarker(isBookmarked, this.selectedPlace, locationManager.getLastLocation());
});
wikipediaButton.setVisibility(place.hasWikipediaLink()?View.VISIBLE:View.GONE);
wikipediaButton.setOnClickListener(view -> Utils.handleWebUrl(getContext(), this.selectedPlace.siteLinks.getWikipediaLink()));
wikidataButton.setVisibility(place.hasWikidataLink()?View.VISIBLE:View.GONE);
wikidataButton.setOnClickListener(view -> Utils.handleWebUrl(getContext(), this.selectedPlace.siteLinks.getWikidataLink()));
directionsButton.setOnClickListener(view -> Utils.handleGeoCoordinates(getActivity(), this.selectedPlace.getLocation()));
commonsButton.setVisibility(this.selectedPlace.hasCommonsLink()?View.VISIBLE:View.GONE);
commonsButton.setOnClickListener(view -> Utils.handleWebUrl(getContext(), this.selectedPlace.siteLinks.getCommonsLink()));
icon.setImageResource(this.selectedPlace.getLabel().getIcon());
title.setText(this.selectedPlace.name);
distance.setText(this.selectedPlace.distance);
description.setText(this.selectedPlace.getLongDescription());
fabCamera.setOnClickListener(view -> {
if (fabCamera.isShown()) {
Timber.d("Camera button tapped. Place: %s", this.selectedPlace.toString());
storeSharedPrefs(selectedPlace);
controller.initiateCameraPick(getActivity());
}
});
fabGallery.setOnClickListener(view -> {
if (fabGallery.isShown()) {
Timber.d("Gallery button tapped. Place: %s", this.selectedPlace.toString());
storeSharedPrefs(selectedPlace);
controller.initiateGalleryPick(getActivity(), false);
}
});
}
private void storeSharedPrefs(Place selectedPlace) {
Timber.d("Store place object %s", selectedPlace.toString());
applicationKvStore.putJson(PLACE_OBJECT, selectedPlace);
}
private void updateBookmarkButtonImage(Place place) {
int bookmarkIcon;
if (bookmarkLocationDao.findBookmarkLocation(place)) {
bookmarkIcon = R.drawable.ic_round_star_filled_24px;
} else {
bookmarkIcon = R.drawable.ic_round_star_border_24px;
}
if (bookmarkButtonImage != null) {
bookmarkButtonImage.setImageResource(bookmarkIcon);
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
wikidataEditListener.setAuthenticationStateListener(this);
}
@Override
public void onDestroy() {
super.onDestroy();
wikidataEditListener.setAuthenticationStateListener(null);
}
@Override
public void onPause() {
super.onPause();
// this means that this activity will not be recreated now, user is leaving it
// or the activity is otherwise finishing
if(getActivity().isFinishing()) {
// we will not need this fragment anymore, this may also be a good place to signal
// to the retained fragment object to perform its own cleanup.
removeMapFragment();
removeListFragment();
}
if (broadcastReceiver != null) {
getActivity().unregisterReceiver(broadcastReceiver);
broadcastReceiver = null;
}
if (locationManager != null && nearbyParentFragmentPresenter != null) {
locationManager.removeLocationListener(nearbyParentFragmentPresenter);
locationManager.unregisterLocationManager();
}
}
@Override
public void onWikidataEditSuccessful() {
if (nearbyMapFragment != null && nearbyParentFragmentPresenter != null && locationManager != null) {
nearbyParentFragmentPresenter.updateMapAndList(MAP_UPDATED, locationManager.getLastLocation());
}
}
private void showErrorMessage(String message) {
ViewUtil.showLongToast(getActivity(), message);
}
}

View file

@ -0,0 +1,391 @@
package fr.free.nrw.commons.nearby.presenter;
import android.view.View;
import com.mapbox.mapboxsdk.annotations.Marker;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.contract.NearbyMapContract;
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
import fr.free.nrw.commons.utils.LocationUtils;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import timber.log.Timber;
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.location.LocationServiceManager.LocationChangeType.SEARCH_CUSTOM_AREA;
public class NearbyParentFragmentPresenter
implements NearbyParentFragmentContract.UserActions,
WikidataEditListener.WikidataP18EditListener,
LocationUpdateListener,
NearbyParentFragmentContract.ViewsAreReadyCallback{
private NearbyParentFragmentContract.View nearbyParentFragmentView;
private NearbyMapContract.View nearbyMapFragmentView;
private NearbyParentFragmentContract.NearbyListView nearbyListFragmentView;
private boolean isNearbyLocked;
private LatLng curLatLng;
private boolean nearbyViewsAreReady;
private boolean onTabSelected;
private boolean mapInitialized;
private Place placeToCenter;
private boolean isPortraitMode;
private boolean placesLoadedOnce;
private LocationServiceManager locationServiceManager;
public static NearbyParentFragmentPresenter presenterInstance;
private NearbyParentFragmentPresenter(NearbyParentFragmentContract.NearbyListView nearbyListFragmentView,
NearbyParentFragmentContract.View nearbyParentFragmentView,
NearbyMapContract.View nearbyMapFragmentView,
LocationServiceManager locationServiceManager) {
this.nearbyListFragmentView = nearbyListFragmentView;
this.nearbyParentFragmentView = nearbyParentFragmentView;
this.nearbyMapFragmentView = nearbyMapFragmentView;
this.nearbyMapFragmentView.viewsAreAssignedToPresenter(this);
this.locationServiceManager = locationServiceManager;
}
// static method to create instance of Singleton class
public static NearbyParentFragmentPresenter getInstance(NearbyParentFragmentContract.NearbyListView nearbyListFragmentView,
NearbyParentFragmentContract.View nearbyParentFragmentView,
NearbyMapContract.View nearbyMapFragmentView,
LocationServiceManager locationServiceManager) {
if (presenterInstance == null) {
presenterInstance = new NearbyParentFragmentPresenter(nearbyListFragmentView,
nearbyParentFragmentView,
nearbyMapFragmentView,
locationServiceManager);
}
return presenterInstance;
}
// We call this method when we are sure presenterInstance is not null
public static NearbyParentFragmentPresenter getInstance() {
return presenterInstance;
}
/**
* Note: To initialize nearby operations both views should be ready and tab is selected.
* Initializes nearby operations if nearby views are ready
*/
@Override
public void onTabSelected() {
Timber.d("Nearby tab selected");
onTabSelected = true;
// The condition for initialize operations is both having views ready and tab is selected
if (nearbyViewsAreReady && !mapInitialized) {
checkForPermission();
}
}
/**
* -To initialize nearby operations both views should be ready and tab is selected.
* Initializes nearby operations if tab selected, otherwise just sets nearby views are ready
*/
@Override
public void nearbyFragmentsAreReady() {
Timber.d("Nearby fragments are ready to be used by presenter");
nearbyViewsAreReady = true;
// The condition for initialize operations is both having views ready and tab is selected
if (onTabSelected) {
checkForPermission();
}
}
/**
* Initializes nearby operations by following these steps:
* -Checks for permission and perform if given
*/
@Override
public void checkForPermission() {
Timber.d("checking for permission");
nearbyParentFragmentView.checkPermissionsAndPerformAction(this::performNearbyOperationsIfPermissionGiven);
}
/**
* - Adds search this area button action
* - Adds camera move action listener
* - Initializes nearby operations, registers listeners, broadcast receivers etc.
*/
public void performNearbyOperationsIfPermissionGiven() {
Timber.d("Permission is given, performing actions");
this.nearbyParentFragmentView.addSearchThisAreaButtonAction();
this.nearbyParentFragmentView.addOnCameraMoveListener(onCameraMove(getMapboxMap()));
initializeMapOperations();
}
public void initializeMapOperations() {
lockUnlockNearby(false);
registerUnregisterLocationListener(false);
nearbyParentFragmentView.addNetworkBroadcastReceiver();
updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED, null);
this.nearbyParentFragmentView.addSearchThisAreaButtonAction();
this.nearbyMapFragmentView.addOnCameraMoveListener(onCameraMove(getMapboxMap()));
mapInitialized = true;
}
/**
* Sets click listeners of FABs, and 2 bottom sheets
*/
@Override
public void setActionListeners(JsonKvStore applicationKvStore) {
nearbyParentFragmentView.setFABPlusAction(v -> {
if (applicationKvStore.getBoolean("login_skipped", false)) {
// prompt the user to login
nearbyParentFragmentView.displayLoginSkippedWarning();
}else {
nearbyParentFragmentView.animateFABs();
}
});
nearbyParentFragmentView.setFABRecenterAction(v -> {
nearbyParentFragmentView.recenterMap(curLatLng);
});
}
@Override
public void backButtonClicked() {
if(nearbyParentFragmentView.isListBottomSheetExpanded()) {
// Back should first hide the bottom sheet if it is expanded
nearbyParentFragmentView.listOptionMenuItemClicked();
} else if (nearbyParentFragmentView.isDetailsBottomSheetVisible()) {
nearbyParentFragmentView.setBottomSheetDetailsSmaller();
} else {
// Otherwise go back to contributions fragment
nearbyParentFragmentView.setTabItemContributions();
}
}
public void markerUnselected() {
nearbyParentFragmentView.hideBottomSheet();
}
public void markerSelected(Marker marker) {
nearbyParentFragmentView.displayBottomSheetWithInfo(marker);
}
/**
* Nearby updates takes time, since they are network operations. During update time, we don't
* want to get any other calls from user. So locking nearby.
* @param isNearbyLocked true means lock, false means unlock
*/
@Override
public void lockUnlockNearby(boolean isNearbyLocked) {
this.isNearbyLocked = isNearbyLocked;
}
public void registerUnregisterLocationListener(boolean removeLocationListener) {
if (removeLocationListener) {
locationServiceManager.unregisterLocationManager();
locationServiceManager.removeLocationListener(this);
Timber.d("Location service manager unregistered and removed");
} else {
locationServiceManager.addLocationListener(this);
nearbyParentFragmentView.registerLocationUpdates(locationServiceManager);
Timber.d("Location service manager added and registered");
}
}
public LatLng getCameraTarget() {
return nearbyMapFragmentView.getCameraTarget();
}
public MapboxMap getMapboxMap() {
return nearbyMapFragmentView.getMapboxMap();
}
/**
* This method should be the single point to update Map and List. Triggered by location
* changes
* @param locationChangeType defines if location changed significantly or slightly
* @param cameraTarget will be used for search this area mode, when searching around
* user's camera target
*/
@Override
public void updateMapAndList(LocationServiceManager.LocationChangeType locationChangeType, LatLng cameraTarget) {
Timber.d("Presenter updates map and list");
if (isNearbyLocked) {
Timber.d("Nearby is locked, so updateMapAndList returns");
return;
}
if (!nearbyParentFragmentView.isNetworkConnectionEstablished()) {
Timber.d("Network connection is not established");
return;
}
LatLng lastLocation = locationServiceManager.getLastLocation();
curLatLng = lastLocation;
if (curLatLng == null) {
Timber.d("Skipping update of nearby places as location is unavailable");
return;
}
/**
* Significant changed - Markers and current location will be updated together
* Slightly changed - Only current position marker will be updated
*/
if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)
|| locationChangeType.equals(MAP_UPDATED)) {
Timber.d("LOCATION_SIGNIFICANTLY_CHANGED");
lockUnlockNearby(true);
nearbyParentFragmentView.setProgressBarVisibility(true);
nearbyParentFragmentView.populatePlaces(lastLocation, lastLocation);
} else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) {
Timber.d("SEARCH_CUSTOM_AREA");
lockUnlockNearby(true);
nearbyParentFragmentView.setProgressBarVisibility(true);
nearbyParentFragmentView.populatePlaces(lastLocation, cameraTarget);
} else { // Means location changed slightly, ie user is walking or driving.
Timber.d("Means location changed slightly");
if (!nearbyParentFragmentView.isSearchThisAreaButtonVisible()) { // Do not track users position if the user is checking around
nearbyParentFragmentView.recenterMap(curLatLng);
}
}
}
/**
* Populates places for custom location, should be used for finding nearby places around a
* location where you are not at.
* @param nearbyPlacesInfo This variable has placeToCenter list information and distances.
*/
public void updateMapMarkers(NearbyController.NearbyPlacesInfo nearbyPlacesInfo, Marker selectedMarker) {
nearbyMapFragmentView.updateMapMarkers(nearbyPlacesInfo.curLatLng, nearbyPlacesInfo.placeList, selectedMarker, this);
nearbyMapFragmentView.addCurrentLocationMarker(nearbyPlacesInfo.curLatLng);
nearbyMapFragmentView.updateMapToTrackPosition(nearbyPlacesInfo.curLatLng);
lockUnlockNearby(false); // So that new location updates wont come
nearbyParentFragmentView.setProgressBarVisibility(false);
nearbyListFragmentView.updateListFragment(nearbyPlacesInfo.placeList);
handleCenteringTaskIfAny();
}
/**
* Populates places for custom location, should be used for finding nearby places around a
* location where you are not at.
* @param nearbyPlacesInfo This variable has placeToCenter list information and distances.
*/
public void updateMapMarkersForCustomLocation(NearbyController.NearbyPlacesInfo nearbyPlacesInfo, Marker selectedMarker) {
nearbyMapFragmentView.updateMapMarkers(nearbyPlacesInfo.curLatLng, nearbyPlacesInfo.placeList, selectedMarker, this);
nearbyMapFragmentView.addCurrentLocationMarker(nearbyPlacesInfo.curLatLng);
lockUnlockNearby(false); // So that new location updates wont come
nearbyParentFragmentView.setProgressBarVisibility(false);
nearbyListFragmentView.updateListFragment(nearbyPlacesInfo.placeList);
handleCenteringTaskIfAny();
}
/**
* Some centering task may need to wait for map to be ready, if they are requested before
* map is ready. So we will remember it when the map is ready
*/
private void handleCenteringTaskIfAny() {
if (!placesLoadedOnce) {
placesLoadedOnce = true;
nearbyMapFragmentView.centerMapToPlace(placeToCenter, isPortraitMode);
}
}
@Override
public void onWikidataEditSuccessful() {
updateMapAndList(MAP_UPDATED, null);
}
@Override
public void onLocationChangedSignificantly(LatLng latLng) {
Timber.d("Location significantly changed");
updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED, null);
}
@Override
public void onLocationChangedSlightly(LatLng latLng) {
Timber.d("Location significantly changed");
updateMapAndList(LOCATION_SLIGHTLY_CHANGED, null);
}
@Override
public void onLocationChangedMedium(LatLng latLng) {
Timber.d("Location changed medium");
}
@Override
public MapboxMap.OnCameraMoveListener onCameraMove(MapboxMap mapboxMap) {
return () -> {
// If our nearby markers are calculated at least once
if (NearbyController.currentLocation != null) {
double distance = mapboxMap.getCameraPosition().target.distanceTo
(LocationUtils.commonsLatLngToMapBoxLatLng(NearbyController.latestSearchLocation));
if (nearbyParentFragmentView.isNetworkConnectionEstablished()) {
if (distance > NearbyController.latestSearchRadius) {
nearbyParentFragmentView.setSearchThisAreaButtonVisibility(true);
} else {
nearbyParentFragmentView.setSearchThisAreaButtonVisibility(false);
}
}
} else {
nearbyParentFragmentView.setSearchThisAreaButtonVisibility(false);
}
};
}
public View.OnClickListener onSearchThisAreaClicked() {
return v -> {
// Lock map operations during search this area operation
nearbyParentFragmentView.setSearchThisAreaButtonVisibility(false);
if (searchCloseToCurrentLocation()){
updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED,
null);
} else {
updateMapAndList(SEARCH_CUSTOM_AREA,
getCameraTarget());
}
};
}
/**
* Returns true if search this area button is used around our current location, so that
* we can continue following our current location again
* @return Returns true if search this area button is used around our current location
*/
public boolean searchCloseToCurrentLocation() {
double distance = LocationUtils.commonsLatLngToMapBoxLatLng(getCameraTarget())
.distanceTo(new com.mapbox.mapboxsdk.geometry.LatLng(NearbyController.currentLocation.getLatitude()
, NearbyController.currentLocation.getLongitude()));
if (distance > NearbyController.currentLocationSearchRadius * 3 / 4) {
return false;
} else {
return true;
}
}
/**
* Centers the map in nearby fragment to a given placeToCenter
* @param place is new center of the map
*/
public void centerMapToPlace(Place place, boolean isPortraitMode) {
if (placesLoadedOnce) {
nearbyMapFragmentView.centerMapToPlace(place, isPortraitMode);
} else {
this.isPortraitMode = isPortraitMode;
this.placeToCenter = place;
}
}
}

View file

@ -18,7 +18,6 @@ import androidx.appcompat.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@ -39,7 +38,6 @@ import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.logging.CommonsLogSender;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.review.ReviewActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import timber.log.Timber;

View file

@ -0,0 +1,51 @@
package fr.free.nrw.commons.utils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
public class NearbyFABUtils {
/*
* Add anchors back before making them visible again.
* */
public static void addAnchorToBigFABs(FloatingActionButton floatingActionButton, int anchorID) {
CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams
(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
params.setAnchorId(anchorID);
params.anchorGravity = Gravity.TOP|Gravity.RIGHT|Gravity.END;
floatingActionButton.setLayoutParams(params);
}
/*
* Add anchors back before making them visible again. Big and small fabs have different anchor
* gravities, therefore the are two methods.
* */
public static void addAnchorToSmallFABs(FloatingActionButton floatingActionButton, int anchorID) {
CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams
(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
params.setAnchorId(anchorID);
params.anchorGravity = Gravity.CENTER_HORIZONTAL;
floatingActionButton.setLayoutParams(params);
}
/*
* We are not able to hide FABs without removing anchors, this method removes anchors
* */
public static void removeAnchorFromFAB(FloatingActionButton floatingActionButton) {
//get rid of anchors
//Somehow this was the only way https://stackoverflow.com/questions/32732932
// /floatingactionbutton-visible-for-sometime-even-if-visibility-is-set-to-gone
CoordinatorLayout.LayoutParams param = (CoordinatorLayout.LayoutParams) floatingActionButton
.getLayoutParams();
param.setAnchorId(View.NO_ID);
// If we don't set them to zero, then they become visible for a moment on upper left side
param.width = 0;
param.height = 0;
floatingActionButton.setLayoutParams(param);
}
}

View file

@ -6,6 +6,11 @@ import android.graphics.Canvas;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import android.util.DisplayMetrics;
import com.mapbox.mapboxsdk.geometry.LatLng;
import java.util.ArrayList;
import java.util.List;
public class UiUtils {
/**
@ -43,4 +48,28 @@ public class UiUtils {
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
return px / ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
/**
* Creates a series of points that create a circle on the map.
* Takes the center latitude, center longitude of the circle,
* the radius in meter and the number of nodes of the circle.
*
* @return List List of LatLng points of the circle.
*/
public static List<com.mapbox.mapboxsdk.geometry.LatLng> createCircleArray(
double centerLat, double centerLong, float radius, int nodes) {
List<com.mapbox.mapboxsdk.geometry.LatLng> circle = new ArrayList<>();
float radiusKilometer = radius / 1000;
double radiusLong = radiusKilometer
/ (111.320 * Math.cos(centerLat * Math.PI / 180));
double radiusLat = radiusKilometer / 110.574;
for (int i = 0; i < nodes; i++) {
double theta = ((double) i / (double) nodes) * (2 * Math.PI);
double nodeLongitude = centerLong + radiusLong * Math.cos(theta);
double nodeLatitude = centerLat + radiusLat * Math.sin(theta);
circle.add(new com.mapbox.mapboxsdk.geometry.LatLng(nodeLatitude, nodeLongitude));
}
return circle;
}
}

View file

@ -11,23 +11,14 @@
android:background="@color/status_bar_blue">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:gravity="center_vertical"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<View
@ -37,6 +28,7 @@
android:layout_below="@id/toolbar"
android:layout_alignParentLeft="true"
android:background="#aa969696"
android:visibility="gone"
android:elevation="6dp">
</View>
@ -49,7 +41,7 @@
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:clickable="true"
android:visibility="invisible"
android:visibility="visible"
app:backgroundTint="@color/main_background_light"
app:elevation="6dp"
app:fabSize="normal"
@ -82,10 +74,11 @@
app:elevation="6dp" />
<ProgressBar
android:id="@+id/search_this_area_button_progress_bar"
android:id="@+id/map_progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:elevation="6dp"
android:visibility="gone" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
@ -100,7 +93,7 @@
android:clickable="true"
android:visibility="invisible"
app:backgroundTint="@color/button_blue"
app:elevation="6dp"
app:elevation="8dp"
app:fabSize="normal"
app:layout_anchor="@id/bottom_sheet_details"
app:layout_anchorGravity="top|right|end"
@ -154,5 +147,4 @@
app:pressedTranslationZ="12dp"
app:srcCompat="@drawable/ic_photo_white_24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -70,4 +70,7 @@
<color name="color_error">#FF0000</color>
<color name="yes_button_color">#B22222</color>
<color name="no_button_color">#006400</color>
<color name="current_marker_stroke">#55000000</color>
<color name="current_marker_fill">#11000000</color>
</resources>

View file

@ -79,7 +79,7 @@ class u {
.thenReturn(Single.just(false))
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
.thenReturn(Single.just(false))
`when`(readFBMD?.processMetadata(ArgumentMatchers.any(),ArgumentMatchers.any()))
`when`(readFBMD?.processMetadata(ArgumentMatchers.any()))
.thenReturn(Single.just(ImageUtils.IMAGE_OK))
`when`(readEXIF?.processMetadata(ArgumentMatchers.anyString()))
.thenReturn(Single.just(ImageUtils.IMAGE_OK))