package org.wikipedia.nearby;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.PointF;
import android.location.Location;
import android.os.Bundle;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import com.mapbox.mapboxsdk.Mapbox;
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.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.constants.MyLocationTracking;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.location.LocationSource;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.maps.Projection;
import com.mapbox.services.android.telemetry.MapboxTelemetry;
import com.mapbox.services.android.telemetry.location.LocationEngineListener;
import org.wikipedia.R;
import org.wikipedia.WikipediaApp;
import org.wikipedia.activity.FragmentUtil;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.history.HistoryEntry;
import org.wikipedia.json.GsonMarshaller;
import org.wikipedia.json.GsonUnmarshaller;
import org.wikipedia.page.PageTitle;
import org.wikipedia.richtext.RichTextUtil;
import org.wikipedia.util.DeviceUtil;
import org.wikipedia.util.FeedbackUtil;
import org.wikipedia.util.PermissionUtil;
import org.wikipedia.util.ResourceUtil;
import org.wikipedia.util.StringUtil;
import org.wikipedia.util.ThrowableUtil;
import org.wikipedia.util.log.L;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
import retrofit2.Call;
/**
* Displays a list of nearby pages.
*/
public class NearbyFragment extends Fragment {
public interface Callback {
void onLoading();
void onLoaded();
void onLoadPage(PageTitle title, int entrySource, @Nullable Location location);
}
private static final String NEARBY_LAST_RESULT = "lastRes";
private static final String NEARBY_LAST_CAMERA_POS = "lastCameraPos";
private static final String NEARBY_FIRST_LOCATION_LOCK = "firstLocationLock";
private static final int GO_TO_LOCATION_PERMISSION_REQUEST = 50;
@BindView(R.id.mapview) MapView mapView;
@BindView(R.id.osm_license) TextView osmLicenseTextView;
private Unbinder unbinder;
@Nullable private MapboxMap mapboxMap;
private Icon markerIconPassive;
private NearbyClient client;
private NearbyResult lastResult;
private LocationChangeListener locationChangeListener = new LocationChangeListener();
@Nullable private CameraPosition lastCameraPos;
private boolean firstLocationLock;
@NonNull public static NearbyFragment newInstance() {
return new NearbyFragment();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
client = new NearbyClient();
Mapbox.getInstance(getContext().getApplicationContext(),
getString(R.string.mapbox_public_token));
MapboxTelemetry.getInstance().setTelemetryEnabled(false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_nearby, container, false);
unbinder = ButterKnife.bind(this, view);
markerIconPassive = IconFactory.getInstance(getContext())
.fromBitmap(ResourceUtil.bitmapFromVectorDrawable(getContext(),
R.drawable.ic_map_marker));
osmLicenseTextView.setText(StringUtil.fromHtml(getString(R.string.nearby_osm_license)));
osmLicenseTextView.setMovementMethod(LinkMovementMethod.getInstance());
RichTextUtil.removeUnderlinesFromLinks(osmLicenseTextView);
mapView.onCreate(savedInstanceState);
setHasOptionsMenu(true);
if (savedInstanceState != null) {
lastCameraPos = savedInstanceState.getParcelable(NEARBY_LAST_CAMERA_POS);
firstLocationLock = savedInstanceState.getBoolean(NEARBY_FIRST_LOCATION_LOCK);
if (savedInstanceState.containsKey(NEARBY_LAST_RESULT)) {
lastResult = GsonUnmarshaller.unmarshal(NearbyResult.class, savedInstanceState.getString(NEARBY_LAST_RESULT));
}
}
LocationSource.getLocationEngine(getContext()).addLocationEngineListener(locationChangeListener);
onLoading();
initializeMap();
return view;
}
@Override
public void onStart() {
mapView.onStart();
super.onStart();
}
@Override
public void onPause() {
if (mapboxMap != null) {
lastCameraPos = mapboxMap.getCameraPosition();
}
mapView.onPause();
super.onPause();
}
@Override
public void onResume() {
mapView.onResume();
super.onResume();
}
@Override
public void onStop() {
mapView.onStop();
super.onStop();
}
@Override
public void onDestroyView() {
LocationSource.getLocationEngine(getContext()).removeLocationEngineListener(locationChangeListener);
mapView.onDestroy();
mapboxMap = null;
unbinder.unbind();
unbinder = null;
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
WikipediaApp.getInstance().getRefWatcher().watch(this);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mapView != null) {
mapView.onSaveInstanceState(outState);
}
outState.putBoolean(NEARBY_FIRST_LOCATION_LOCK, firstLocationLock);
if (mapboxMap != null) {
outState.putParcelable(NEARBY_LAST_CAMERA_POS, mapboxMap.getCameraPosition());
}
if (lastResult != null) {
outState.putString(NEARBY_LAST_RESULT, GsonMarshaller.marshal(lastResult));
}
}
@Override public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (mapView == null || mapboxMap == null) {
return;
}
if (isVisibleToUser && !firstLocationLock) {
goToUserLocationOrPromptPermissions();
}
}
@Override
public void onLowMemory() {
super.onLowMemory();
if (mapView != null) {
mapView.onLowMemory();
}
}
@OnClick(R.id.user_location_button) void onClick() {
if (!locationPermitted()) {
requestLocationRuntimePermissions(GO_TO_LOCATION_PERMISSION_REQUEST);
} else if (mapboxMap != null) {
goToUserLocation();
}
}
private void initializeMap() {
mapView.getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(@NonNull MapboxMap mapboxMap) {
NearbyFragment.this.mapboxMap = mapboxMap;
enableUserLocationMarker();
mapboxMap.getTrackingSettings().setMyLocationTrackingMode(MyLocationTracking.TRACKING_NONE);
mapboxMap.setOnScrollListener(new MapboxMap.OnScrollListener() {
@Override
public void onScroll() {
fetchNearbyPages();
}
});
mapboxMap.setOnMarkerClickListener(new MapboxMap.OnMarkerClickListener() {
@Override
public boolean onMarkerClick(@NonNull Marker marker) {
NearbyPage page = findNearbyPageFromMarker(marker);
if (page != null) {
PageTitle title = new PageTitle(page.getTitle(), lastResult.getWiki(), page.getThumbUrl());
onLoadPage(title, HistoryEntry.SOURCE_NEARBY, page.getLocation());
return true;
} else {
return false;
}
}
});
if (lastCameraPos != null) {
mapboxMap.setCameraPosition(lastCameraPos);
} else {
goToUserLocationOrPromptPermissions();
}
if (lastResult != null) {
showNearbyPages(lastResult);
}
}
});
}
@Nullable
private NearbyPage findNearbyPageFromMarker(Marker marker) {
for (NearbyPage page : lastResult.getList()) {
if (page.getTitle().equals(marker.getTitle())) {
return page;
}
}
return null;
}
private boolean locationPermitted() {
return ContextCompat.checkSelfPermission(WikipediaApp.getInstance(),
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
private void requestLocationRuntimePermissions(int requestCode) {
requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, requestCode);
// once permission is granted/denied it will continue with onRequestPermissionsResult
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
switch (requestCode) {
case GO_TO_LOCATION_PERMISSION_REQUEST:
if (PermissionUtil.isPermitted(grantResults) && mapboxMap != null) {
goToUserLocation();
} else {
onLoaded();
FeedbackUtil.showMessage(getActivity(), R.string.nearby_zoom_to_location);
}
break;
default:
throw new RuntimeException("unexpected permission request code " + requestCode);
}
}
private void enableUserLocationMarker() {
if (mapboxMap != null && locationPermitted()) {
mapboxMap.setMyLocationEnabled(true);
}
}
private void goToUserLocation() {
if (mapboxMap == null || !getUserVisibleHint()) {
return;
}
if (!DeviceUtil.isLocationServiceEnabled(getContext().getApplicationContext())) {
showLocationDisabledSnackbar();
return;
}
enableUserLocationMarker();
Location location = mapboxMap.getMyLocation();
if (location != null) {
goToLocation(location);
}
fetchNearbyPages();
}
private void goToLocation(@NonNull Location location) {
if (mapboxMap == null) {
return;
}
CameraPosition pos = new CameraPosition.Builder()
.target(new LatLng(location))
.zoom(getResources().getInteger(R.integer.map_default_zoom))
.build();
mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(pos));
}
private void goToUserLocationOrPromptPermissions() {
if (locationPermitted()) {
goToUserLocation();
} else if (getUserVisibleHint()) {
showLocationPermissionSnackbar();
}
}
private void showLocationDisabledSnackbar() {
Snackbar snackbar = FeedbackUtil.makeSnackbar(getActivity(),
getString(R.string.location_service_disabled),
FeedbackUtil.LENGTH_DEFAULT);
snackbar.setAction(R.string.enable_location_service, new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent settingsIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
getContext().startActivity(settingsIntent);
}
});
snackbar.show();
}
private void showLocationPermissionSnackbar() {
Snackbar snackbar = FeedbackUtil.makeSnackbar(getActivity(),
getString(R.string.location_permissions_enable_prompt),
FeedbackUtil.LENGTH_DEFAULT);
snackbar.setAction(R.string.location_permissions_enable_action, new View.OnClickListener() {
@Override
public void onClick(View v) {
requestLocationRuntimePermissions(GO_TO_LOCATION_PERMISSION_REQUEST);
}
});
snackbar.show();
}
private void fetchNearbyPages() {
final int fetchTaskDelayMillis = 500;
mapView.removeCallbacks(fetchTaskRunnable);
mapView.postDelayed(fetchTaskRunnable, fetchTaskDelayMillis);
}
private Runnable fetchTaskRunnable = new Runnable() {
@Override
public void run() {
if (!isResumed() || mapboxMap == null) {
return;
}
onLoading();
WikiSite wiki = WikipediaApp.getInstance().getWikiSite();
client.request(wiki, mapboxMap.getCameraPosition().target.getLatitude(),
mapboxMap.getCameraPosition().target.getLongitude(), getMapRadius(),
new NearbyClient.Callback() {
@Override public void success(@NonNull Call<MwQueryResponse<Nearby>> call,
@NonNull NearbyResult result) {
if (!isResumed()) {
return;
}
lastResult = result;
showNearbyPages(result);
onLoaded();
}
@Override public void failure(@NonNull Call<MwQueryResponse<Nearby>> call,
@NonNull Throwable caught) {
if (!isResumed()) {
return;
}
ThrowableUtil.AppError error = ThrowableUtil.getAppError(getActivity(), caught);
Toast.makeText(getActivity(), error.getError(), Toast.LENGTH_SHORT).show();
L.e(caught);
onLoaded();
}
});
}
};
private double getMapRadius() {
if (mapboxMap == null) {
return 0;
}
Projection proj = mapboxMap.getProjection();
LatLng leftTop = proj.fromScreenLocation(new PointF(0.0f, 0.0f));
LatLng rightTop = proj.fromScreenLocation(new PointF(mapView.getWidth(), 0.0f));
LatLng leftBottom = proj.fromScreenLocation(new PointF(0.0f, mapView.getHeight()));
double width = leftTop.distanceTo(rightTop);
double height = leftTop.distanceTo(leftBottom);
return Math.max(width, height) / 2;
}
private void showNearbyPages(NearbyResult result) {
if (mapboxMap == null) {
return;
}
getActivity().invalidateOptionsMenu();
// Since Marker is a descendant of Annotation, this will remove all Markers.
mapboxMap.removeAnnotations();
List<MarkerOptions> optionsList = new ArrayList<>();
for (NearbyPage item : result.getList()) {
if (item.getLocation() != null) {
optionsList.add(createMarkerOptions(item));
}
}
mapboxMap.addMarkers(optionsList);
}
@NonNull
private MarkerOptions createMarkerOptions(NearbyPage page) {
Location location = page.getLocation();
return new MarkerOptions()
.position(new LatLng(location.getLatitude(), location.getLongitude()))
.title(page.getTitle())
.icon(markerIconPassive);
}
private void onLoading() {
Callback callback = callback();
if (callback != null) {
callback.onLoading();
}
}
private void onLoaded() {
Callback callback = callback();
if (callback != null) {
callback.onLoaded();
}
}
private void onLoadPage(@NonNull PageTitle title, int entrySource, @Nullable Location location) {
Callback callback = callback();
if (callback != null) {
callback.onLoadPage(title, entrySource, location);
}
}
private class LocationChangeListener implements LocationEngineListener {
@Override
public void onConnected() {
}
@Override
public void onLocationChanged(Location location) {
if (!firstLocationLock) {
goToUserLocation();
firstLocationLock = true;
}
}
}
@Nullable
private Callback callback() {
return FragmentUtil.getCallback(this, Callback.class);
}
}