diff --git a/app/build.gradle b/app/build.gradle index 4c562e7de..752f4b718 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,7 +39,7 @@ 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-sdk:8.6.2' + implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.1.0' implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-localization-v8:0.11.0' implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-scalebar-v9:0.4.0' implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 47d102fdc..036299c66 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -137,7 +137,11 @@ android:name=".review.ReviewActivity" android:label="@string/title_activity_review" /> - + + diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java new file mode 100644 index 000000000..bd890d6ef --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java @@ -0,0 +1,54 @@ +package fr.free.nrw.commons.LocationPicker; + +import android.app.Activity; +import android.content.Intent; +import androidx.annotation.NonNull; +import com.mapbox.mapboxsdk.camera.CameraPosition; + +/** + * Helper class for starting the activity + */ +public final class LocationPicker { + + /** + * Getting camera position from the intent using constants + * @param data intent + * @return CameraPosition + */ + public static CameraPosition getCameraPosition(final Intent data) { + return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); + } + + public static class IntentBuilder { + + private final Intent intent; + + /** + * Creates a new builder that creates an intent to launch the place picker activity. + */ + public IntentBuilder() { + intent = new Intent(); + } + + /** + * Gets and puts location in intent + * @param position CameraPosition + * @return LocationPicker.IntentBuilder + */ + public LocationPicker.IntentBuilder defaultLocation( + final CameraPosition position) { + intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position); + return this; + } + + /** + * Gets and sets the activity + * @param activity Activity + * @return Intent + */ + public Intent build(final Activity activity) { + intent.setClass(activity, LocationPickerActivity.class); + return intent; + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java new file mode 100644 index 000000000..57da44a42 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java @@ -0,0 +1,288 @@ +package fr.free.nrw.commons.LocationPicker; + +import android.content.Intent; +import android.os.Bundle; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.Window; +import android.view.animation.OvershootInterpolator; +import android.widget.ImageView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.mapbox.android.core.permissions.PermissionsManager; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.camera.CameraPosition.Builder; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.location.LocationComponent; +import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions; +import com.mapbox.mapboxsdk.location.modes.CameraMode; +import com.mapbox.mapboxsdk.location.modes.RenderMode; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraIdleListener; +import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener; +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; +import com.mapbox.mapboxsdk.maps.Style; +import com.mapbox.mapboxsdk.maps.UiSettings; +import fr.free.nrw.commons.R; +import org.jetbrains.annotations.NotNull; +import timber.log.Timber; + +/** + * Helps to pick location and return the result with an intent + */ +public class LocationPickerActivity extends AppCompatActivity implements OnMapReadyCallback, + OnCameraMoveStartedListener, OnCameraIdleListener, Observer { + + /** + * cameraPosition : position of picker + */ + private CameraPosition cameraPosition; + /** + * markerImage : picker image + */ + private ImageView markerImage; + /** + * mapboxMap : map + */ + private MapboxMap mapboxMap; + /** + * mapView : view of the map + */ + private MapView mapView; + /** + * tvAttribution : credit + */ + private AppCompatTextView tvAttribution; + + @Override + protected void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getWindow().requestFeature(Window.FEATURE_ACTION_BAR); + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.hide(); + } + setContentView(R.layout.activity_location_picker); + + if (savedInstanceState == null) { + cameraPosition = getIntent().getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); + } + + final LocationPickerViewModel viewModel = new ViewModelProvider(this) + .get(LocationPickerViewModel.class); + viewModel.getResult().observe(this, this); + + bindViews(); + addBackButtonListener(); + addPlaceSelectedButton(); + addCredits(); + getToolbarUI(); + + mapView.onCreate(savedInstanceState); + mapView.getMapAsync(this); + } + + /** + * For showing credits + */ + private void addCredits() { + tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution))); + tvAttribution.setMovementMethod(LinkMovementMethod.getInstance()); + } + + /** + * Clicking back button destroy locationPickerActivity + */ + private void addBackButtonListener() { + final ImageView backButton = findViewById(R.id.mapbox_place_picker_toolbar_back_button); + backButton.setOnClickListener(view -> finish()); + } + + /** + * Binds mapView and location picker icon + */ + private void bindViews() { + mapView = findViewById(R.id.map_view); + markerImage = findViewById(R.id.location_picker_image_view_marker); + tvAttribution = findViewById(R.id.tv_attribution); + } + + /** + * Binds the listeners + */ + private void bindListeners() { + mapboxMap.addOnCameraMoveStartedListener( + this); + mapboxMap.addOnCameraIdleListener( + this); + } + + /** + * Gets toolbar color + */ + private void getToolbarUI() { + final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar); + toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor)); + } + + /** + * Takes action when map is ready to show + * @param mapboxMap map + */ + @Override + public void onMapReady(final MapboxMap mapboxMap) { + this.mapboxMap = mapboxMap; + mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> { + adjustCameraBasedOnOptions(); + bindListeners(); + enableLocationComponent(style); + }); + } + + /** + * move the location to the current media coordinates + */ + private void adjustCameraBasedOnOptions() { + mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); + } + + /** + * Enables location components + * @param loadedMapStyle Style + */ + @SuppressWarnings( {"MissingPermission"}) + private void enableLocationComponent(@NonNull final Style loadedMapStyle) { + final UiSettings uiSettings = mapboxMap.getUiSettings(); + uiSettings.setAttributionEnabled(false); + + // Check if permissions are enabled and if not request + if (PermissionsManager.areLocationPermissionsGranted(this)) { + + // Get an instance of the component + final LocationComponent locationComponent = mapboxMap.getLocationComponent(); + + // Activate with options + locationComponent.activateLocationComponent( + LocationComponentActivationOptions.builder(this, loadedMapStyle).build()); + + // Enable to make component visible + locationComponent.setLocationComponentEnabled(true); + + // Set the component's camera mode + locationComponent.setCameraMode(CameraMode.NONE); + + // Set the component's render mode + locationComponent.setRenderMode(RenderMode.NORMAL); + + } + } + + /** + * Acts on camera moving + * @param reason int + */ + @Override + public void onCameraMoveStarted(final int reason) { + Timber.v("Map camera has begun moving."); + if (markerImage.getTranslationY() == 0) { + markerImage.animate().translationY(-75) + .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); + } + } + + /** + * Acts on camera idle + */ + @Override + public void onCameraIdle() { + Timber.v("Map camera is now idling."); + markerImage.animate().translationY(0) + .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); + } + + /** + * Takes action on camera position + * @param position position of picker + */ + @Override + public void onChanged(@Nullable CameraPosition position) { + if (position == null) { + position = new Builder() + .target(new LatLng(mapboxMap.getCameraPosition().target.getLatitude(), + mapboxMap.getCameraPosition().target.getLongitude())) + .zoom(16).build(); + } + cameraPosition = position; + } + + /** + * Select the preferable location + */ + private void addPlaceSelectedButton() { + final FloatingActionButton placeSelectedButton = findViewById(R.id.location_chosen_button); + placeSelectedButton.setOnClickListener(view -> placeSelected()); + } + + /** + * Return the intent with required data + */ + void placeSelected() { + final Intent returningIntent = new Intent(); + returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, + mapboxMap.getCameraPosition()); + setResult(AppCompatActivity.RESULT_OK, returningIntent); + finish(); + } + + @Override + protected void onStart() { + super.onStart(); + mapView.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + mapView.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + mapView.onPause(); + } + + @Override + protected void onStop() { + super.onStop(); + mapView.onStop(); + } + + @Override + protected void onSaveInstanceState(final @NotNull Bundle outState) { + super.onSaveInstanceState(outState); + mapView.onSaveInstanceState(outState); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mapView.onDestroy(); + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + mapView.onLowMemory(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java new file mode 100644 index 000000000..1a2cab6d1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.LocationPicker; + +/** + * Constants need for location picking + */ +public final class LocationPickerConstants { + + public static final String MAP_CAMERA_POSITION + = "location.picker.cameraPosition"; + + private LocationPickerConstants() { + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java new file mode 100644 index 000000000..c0b91cf71 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java @@ -0,0 +1,60 @@ +package fr.free.nrw.commons.LocationPicker; + +import android.app.Application; +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.MutableLiveData; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import org.jetbrains.annotations.NotNull; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import timber.log.Timber; + +/** + * Observes live camera position data + */ +public class LocationPickerViewModel extends AndroidViewModel implements Callback { + + /** + * Wrapping CameraPosition with MutableLiveData + */ + private final MutableLiveData result = new MutableLiveData<>(); + + /** + * Constructor for this class + * @param application Application + */ + public LocationPickerViewModel(@NonNull final Application application) { + super(application); + } + + /** + * Responses on camera position changing + * @param call Call + * @param response Response + */ + @Override + public void onResponse(final @NotNull Call call, + final Response response) { + if(response.body()==null){ + result.setValue(null); + return; + } + result.setValue(response.body()); + } + + @Override + public void onFailure(final @NotNull Call call, final @NotNull Throwable t) { + Timber.e(t); + } + + /** + * Gets live CameraPosition + * @return MutableLiveData + */ + public MutableLiveData getResult() { + return result; + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/Media.kt b/app/src/main/java/fr/free/nrw/commons/Media.kt index 090d8e230..3987acb37 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.kt +++ b/app/src/main/java/fr/free/nrw/commons/Media.kt @@ -74,7 +74,7 @@ class Media constructor( * Gets the coordinates of where the file was created. * @return file coordinates as a LatLng */ - val coordinates: LatLng? = null, + var coordinates: LatLng? = null, val captions: Map = emptyMap(), val descriptions: Map = emptyMap(), val depictionIds: List = emptyList() diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt index 0ae7e42de..4dfce0f88 100644 --- a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt @@ -1,6 +1,7 @@ package fr.free.nrw.commons.actions import io.reactivex.Observable +import io.reactivex.Single import org.wikipedia.csrf.CsrfTokenClient /** @@ -62,4 +63,15 @@ class PageEditClient( Observable.just(false) } } + + /** + * Get whole WikiText of required file + * @param title : Name of the file + * @return Observable + */ + fun getCurrentWikiText(title: String): Single { + return pageEditInterface.getWikiText(title).map { + it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content() + } + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.kt b/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.kt index 8b27d1288..91e8230bf 100644 --- a/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.kt +++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.kt @@ -1,12 +1,11 @@ package fr.free.nrw.commons.actions import io.reactivex.Observable +import io.reactivex.Single import org.wikipedia.dataclient.Service +import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.edit.Edit -import retrofit2.http.Field -import retrofit2.http.FormUrlEncoded -import retrofit2.http.Headers -import retrofit2.http.POST +import retrofit2.http.* /** * This interface facilitates wiki commons page editing services to the Networking module @@ -73,4 +72,17 @@ interface PageEditInterface { @Field("prependtext") prependText: String, @Field("token") token: String ): Observable + + /** + * Get wiki text for provided file names + * @param titles : Name of the file + * @return Single + */ + @GET( + Service.MW_API_PREFIX + + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=" + ) + fun getWikiText( + @Query("titles") title: String + ): Single } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/coordinates/CoordinateEditHelper.java b/app/src/main/java/fr/free/nrw/commons/coordinates/CoordinateEditHelper.java new file mode 100644 index 000000000..1f3a75029 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/coordinates/CoordinateEditHelper.java @@ -0,0 +1,197 @@ +package fr.free.nrw.commons.coordinates; + +import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_EDIT_COORDINATES; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.actions.PageEditClient; +import fr.free.nrw.commons.notification.NotificationHelper; +import fr.free.nrw.commons.utils.ViewUtilWrapper; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; +import java.util.Objects; +import javax.inject.Inject; +import javax.inject.Named; +import org.apache.commons.lang3.StringUtils; +import timber.log.Timber; + +/** + * Helper class for edit and update given coordinates and showing notification about new coordinates + * upgradation + */ +public class CoordinateEditHelper { + + /** + * notificationHelper: helps creating notification + */ + private final NotificationHelper notificationHelper; + /** + * * pageEditClient: methods provided by this member posts the edited coordinates + * to the Media wiki api + */ + public final PageEditClient pageEditClient; + /** + * viewUtil: helps to show Toast + */ + private final ViewUtilWrapper viewUtil; + + @Inject + public CoordinateEditHelper(final NotificationHelper notificationHelper, + @Named("commons-page-edit") final PageEditClient pageEditClient, + final ViewUtilWrapper viewUtil) { + this.notificationHelper = notificationHelper; + this.pageEditClient = pageEditClient; + this.viewUtil = viewUtil; + } + + /** + * Public interface to edit coordinates + * @param context to be added + * @param media to be added + * @param Accuracy to be added + * @return Single + */ + public Single makeCoordinatesEdit(final Context context, final Media media, + final String Latitude, final String Longitude, final String Accuracy) { + viewUtil.showShortToast(context, + context.getString(R.string.coordinates_edit_helper_make_edit_toast)); + return addCoordinates(media, Latitude, Longitude, Accuracy) + .flatMapSingle(result -> Single.just(showCoordinatesEditNotification(context, media, + Latitude, Longitude, Accuracy, result))) + .firstOrError(); + } + + /** + * Replaces new coordinates + * @param media to be added + * @param Latitude to be added + * @param Longitude to be added + * @param Accuracy to be added + * @return Observable + */ + private Observable addCoordinates(final Media media, final String Latitude, + final String Longitude, final String Accuracy) { + Timber.d("thread is coordinates adding %s", Thread.currentThread().getName()); + final String summary = "Adding Coordinates"; + + final StringBuilder buffer = new StringBuilder(); + + final String wikiText = pageEditClient.getCurrentWikiText(media.getFilename()) + .subscribeOn(Schedulers.io()) + .blockingGet(); + + if (Latitude != null) { + buffer.append("\n{{Location|").append(Latitude).append("|").append(Longitude) + .append("|").append(Accuracy).append("}}"); + } + + final String editedLocation = buffer.toString(); + final String appendText = getFormattedWikiText(wikiText, editedLocation); + + return pageEditClient.edit(Objects.requireNonNull(media.getFilename()) + , appendText, summary); + } + + /** + * Helps to get formatted wikitext with upgraded location + * @param wikiText current wikitext + * @param editedLocation new location + * @return String + */ + private String getFormattedWikiText(final String wikiText, final String editedLocation){ + + if (wikiText.contains("filedesc") && wikiText.contains("Location")) { + + final String fromLocationToEnd = wikiText.substring(wikiText.indexOf("{{Location")); + final String firstHalf = wikiText.substring(0, wikiText.indexOf("{{Location")); + final String lastHalf = fromLocationToEnd.substring( + fromLocationToEnd.indexOf("}}") + 2); + + final int startOfSecondSection = StringUtils.ordinalIndexOf(wikiText, + "==", 3); + final StringBuilder buffer = new StringBuilder(); + if (wikiText.charAt(wikiText.indexOf("{{Location")-1) == '\n') { + buffer.append(editedLocation.substring(1)); + } else { + buffer.append(editedLocation); + } + if (startOfSecondSection != -1 && wikiText.charAt(startOfSecondSection-1)!= '\n') { + buffer.append("\n"); + } + + return firstHalf + buffer + lastHalf; + + } + if (wikiText.contains("filedesc") && !wikiText.contains("Location")) { + + final int startOfSecondSection = StringUtils.ordinalIndexOf(wikiText, + "==", 3); + + if (startOfSecondSection != -1) { + final String firstHalf = wikiText.substring(0, startOfSecondSection); + final String lastHalf = wikiText.substring(startOfSecondSection); + final String buffer = editedLocation.substring(1) + + "\n"; + return firstHalf + buffer + lastHalf; + } + + return wikiText + editedLocation; + } + if (!wikiText.contains("filedesc") && !wikiText.contains("Location")) { + + return "== {{int:filedesc}} ==" + editedLocation + wikiText; + + } + if (!wikiText.contains("filedesc") && wikiText.contains("Location")) { + + return "== {{int:filedesc}} ==" + editedLocation + wikiText; + + } + return null; + } + + /** + * Update coordinates and shows notification about coordinates update + * @param context to be added + * @param media to be added + * @param latitude to be added + * @param longitude to be added + * @param Accuracy to be added + * @param result to be added + * @return boolean + */ + private boolean showCoordinatesEditNotification(final Context context, final Media media, + final String latitude, final String longitude, final String Accuracy, + final boolean result) { + final String message; + String title = context.getString(R.string.coordinates_edit_helper_show_edit_title); + + if (result) { + media.setCoordinates( + new fr.free.nrw.commons.location.LatLng(Double.parseDouble(latitude), + Double.parseDouble(longitude), + Float.parseFloat(Accuracy))); + title += ": " + context + .getString(R.string.coordinates_edit_helper_show_edit_title_success); + final StringBuilder coordinatesInMessage = new StringBuilder(); + final String mediaCoordinate = String.valueOf(media.getCoordinates()); + coordinatesInMessage.append(mediaCoordinate); + message = context.getString(R.string.coordinates_edit_helper_show_edit_message, + coordinatesInMessage.toString()); + } else { + title += ": " + context.getString(R.string.coordinates_edit_helper_show_edit_title); + message = context.getString(R.string.coordinates_edit_helper_edit_message_else) ; + } + + final String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename(); + final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile)); + notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_COORDINATES, + browserIntent); + return result; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 6c9472ddd..8e6518c44 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -1,5 +1,7 @@ package fr.free.nrw.commons.media; +import static android.app.Activity.RESULT_CANCELED; +import static android.app.Activity.RESULT_OK; import static android.view.View.GONE; import static android.view.View.VISIBLE; import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_NEEDING_CATEGORIES; @@ -16,11 +18,8 @@ import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; -import android.util.Log; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnKeyListener; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; @@ -50,6 +49,9 @@ import com.facebook.imagepipeline.image.ImageInfo; import com.facebook.imagepipeline.request.ImageRequest; import com.jakewharton.rxbinding2.view.RxView; import com.jakewharton.rxbinding2.widget.RxSearchView; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.geometry.LatLng; +import fr.free.nrw.commons.LocationPicker.LocationPicker; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.R; @@ -62,10 +64,11 @@ import fr.free.nrw.commons.category.CategoryEditHelper; import fr.free.nrw.commons.category.CategoryEditSearchRecyclerViewAdapter; import fr.free.nrw.commons.category.CategoryEditSearchRecyclerViewAdapter.Callback; import fr.free.nrw.commons.contributions.ContributionsFragment; +import fr.free.nrw.commons.coordinates.CoordinateEditHelper; import fr.free.nrw.commons.delete.DeleteHelper; import fr.free.nrw.commons.delete.ReasonBuilder; -import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.nearby.Label; import fr.free.nrw.commons.ui.widget.HtmlTextView; @@ -77,8 +80,9 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; -import java.util.concurrent.TimeUnit; import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Named; import org.apache.commons.lang3.StringUtils; @@ -88,6 +92,7 @@ import timber.log.Timber; public class MediaDetailFragment extends CommonsDaggerSupportFragment implements Callback, CategoryEditHelper.Callback { + private static final int REQUEST_CODE = 1001 ; private boolean editable; private boolean isCategoryImage; private MediaDetailPagerFragment.MediaDetailProvider detailProvider; @@ -125,6 +130,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements @Inject CategoryEditHelper categoryEditHelper; @Inject + CoordinateEditHelper coordinateEditHelper; + @Inject ViewUtilWrapper viewUtil; @Inject CategoryClient categoryClient; @@ -760,6 +767,75 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements } } + @OnClick(R.id.coordinate_edit) + public void onUpdateCoordinatesClicked(){ + goToLocationPickerActivity(); + } + + /** + * Start location picker activity with a request code and get the coordinates from the activity. + */ + private void goToLocationPickerActivity() { + /* + If location is not provided in media this coordinates will act as a placeholder in + location picker activity + */ + double defaultLatitude = 37.773972; + double defaultLongitude = -122.431297; + + if (media.getCoordinates() != null) { + defaultLatitude = media.getCoordinates().getLatitude(); + defaultLongitude = media.getCoordinates().getLongitude(); + } + startActivityForResult(new LocationPicker.IntentBuilder() + .defaultLocation(new CameraPosition.Builder() + .target(new LatLng(defaultLatitude, defaultLongitude)) + .zoom(16).build()) + .build(getActivity()), REQUEST_CODE); + } + + /** + * Get the coordinates and update the existing coordinates. + * @param requestCode + * @param resultCode + * @param data + */ + @Override + public void onActivityResult(final int requestCode, final int resultCode, + @Nullable final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) { + + assert data != null; + final CameraPosition cameraPosition = LocationPicker.getCameraPosition(data); + + if (cameraPosition != null) { + + final String latitude = String.valueOf(cameraPosition.target.getLatitude()); + final String longitude = String.valueOf(cameraPosition.target.getLongitude()); + final String accuracy = String.valueOf(cameraPosition.target.getAltitude()); + String currentLatitude = null; + String currentLongitude = null; + + if (media.getCoordinates() != null) { + currentLatitude = String.valueOf(media.getCoordinates().getLatitude()); + currentLongitude = String.valueOf(media.getCoordinates().getLongitude()); + } + + if (!latitude.equals(currentLatitude) || !longitude.equals(currentLongitude)) { + updateCoordinates(latitude, longitude, accuracy); + } else if (media.getCoordinates() == null) { + updateCoordinates(latitude, longitude, accuracy); + } + } + } else if (resultCode == RESULT_CANCELED) { + viewUtil.showShortToast(getContext(), + Objects.requireNonNull(getContext()) + .getString(R.string.coordinates_picking_unsuccessful)); + } + } + @OnClick(R.id.update_categories_button) public void onUpdateCategoriesClicked() { updateCategories(categoryEditSearchRecyclerViewAdapter.getNewCategories()); @@ -783,6 +859,24 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements })); } + /** + * Fetched coordinates are replaced with existing coordinates by a POST API call. + * @param Latitude to be added + * @param Longitude to be added + * @param Accuracy to be added + */ + public void updateCoordinates(final String Latitude, final String Longitude, + final String Accuracy) { + compositeDisposable.add(coordinateEditHelper.makeCoordinatesEdit(getContext(), media, + Latitude, Longitude, Accuracy) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(s -> { + Timber.d("Coordinates are added."); + coordinates.setText(prettyCoordinates(media)); + })); + } + @SuppressLint("StringFormatInvalid") @OnClick(R.id.nominateDeletion) public void onDeleteButtonClicked(){ diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java index 76b51bb57..a23f350bd 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java @@ -25,6 +25,7 @@ public class NotificationHelper { public static final int NOTIFICATION_DELETE = 1; public static final int NOTIFICATION_EDIT_CATEGORY = 2; + public static final int NOTIFICATION_EDIT_COORDINATES = 3; private NotificationManager notificationManager; private NotificationCompat.Builder notificationBuilder; diff --git a/app/src/main/res/drawable-hdpi/map_default_map_marker.png b/app/src/main/res/drawable-hdpi/map_default_map_marker.png new file mode 100644 index 000000000..ef993a0f5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/map_default_map_marker.png differ diff --git a/app/src/main/res/drawable-hdpi/map_default_map_marker_shadow.png b/app/src/main/res/drawable-hdpi/map_default_map_marker_shadow.png new file mode 100644 index 000000000..c841453c1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/map_default_map_marker_shadow.png differ diff --git a/app/src/main/res/drawable-hdpi/mapbox_logo_icon.png b/app/src/main/res/drawable-hdpi/mapbox_logo_icon.png new file mode 100644 index 000000000..7568387a8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/mapbox_logo_icon.png differ diff --git a/app/src/main/res/drawable-mdpi/map_default_map_marker.png b/app/src/main/res/drawable-mdpi/map_default_map_marker.png new file mode 100644 index 000000000..45ca4ab58 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/map_default_map_marker.png differ diff --git a/app/src/main/res/drawable-mdpi/map_default_map_marker_shadow.png b/app/src/main/res/drawable-mdpi/map_default_map_marker_shadow.png new file mode 100644 index 000000000..e8b49eefe Binary files /dev/null and b/app/src/main/res/drawable-mdpi/map_default_map_marker_shadow.png differ diff --git a/app/src/main/res/drawable-mdpi/mapbox_logo_icon.png b/app/src/main/res/drawable-mdpi/mapbox_logo_icon.png new file mode 100644 index 000000000..9cdec5151 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/mapbox_logo_icon.png differ diff --git a/app/src/main/res/drawable-xhdpi/map_default_map_marker.png b/app/src/main/res/drawable-xhdpi/map_default_map_marker.png new file mode 100644 index 000000000..19a1f66a8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/map_default_map_marker.png differ diff --git a/app/src/main/res/drawable-xhdpi/map_default_map_marker_shadow.png b/app/src/main/res/drawable-xhdpi/map_default_map_marker_shadow.png new file mode 100644 index 000000000..b994fc39d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/map_default_map_marker_shadow.png differ diff --git a/app/src/main/res/drawable-xhdpi/mapbox_logo_icon.png b/app/src/main/res/drawable-xhdpi/mapbox_logo_icon.png new file mode 100644 index 000000000..7eec45f4b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/mapbox_logo_icon.png differ diff --git a/app/src/main/res/drawable-xxhdpi/map_default_map_marker.png b/app/src/main/res/drawable-xxhdpi/map_default_map_marker.png new file mode 100644 index 000000000..4a9abb9ee Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/map_default_map_marker.png differ diff --git a/app/src/main/res/drawable-xxhdpi/map_default_map_marker_shadow.png b/app/src/main/res/drawable-xxhdpi/map_default_map_marker_shadow.png new file mode 100644 index 000000000..7218f0caf Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/map_default_map_marker_shadow.png differ diff --git a/app/src/main/res/drawable-xxhdpi/mapbox_logo_icon.png b/app/src/main/res/drawable-xxhdpi/mapbox_logo_icon.png new file mode 100644 index 000000000..5876056f1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/mapbox_logo_icon.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/map_default_map_marker.png b/app/src/main/res/drawable-xxxhdpi/map_default_map_marker.png new file mode 100644 index 000000000..65fe36d1b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/map_default_map_marker.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/map_default_map_marker_shadow.png b/app/src/main/res/drawable-xxxhdpi/map_default_map_marker_shadow.png new file mode 100644 index 000000000..b0027d86b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/map_default_map_marker_shadow.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/mapbox_logo_icon.png b/app/src/main/res/drawable-xxxhdpi/mapbox_logo_icon.png new file mode 100644 index 000000000..e5780ccc0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/mapbox_logo_icon.png differ diff --git a/app/src/main/res/layout/activity_location_picker.xml b/app/src/main/res/layout/activity_location_picker.xml new file mode 100644 index 000000000..a6aa85d71 --- /dev/null +++ b/app/src/main/res/layout/activity_location_picker.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_container_location_picker.xml b/app/src/main/res/layout/bottom_container_location_picker.xml new file mode 100644 index 000000000..95c0a2431 --- /dev/null +++ b/app/src/main/res/layout/bottom_container_location_picker.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_location_picker.xml b/app/src/main/res/layout/content_location_picker.xml new file mode 100644 index 000000000..2f846759c --- /dev/null +++ b/app/src/main/res/layout/content_location_picker.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_media_detail.xml b/app/src/main/res/layout/fragment_media_detail.xml index d4fb2d18e..ef691c300 100644 --- a/app/src/main/res/layout/fragment_media_detail.xml +++ b/app/src/main/res/layout/fragment_media_detail.xml @@ -249,6 +249,16 @@ android:drawablePadding="@dimen/tiny_gap" android:drawableStart="?attr/iconMap24" tools:text="Coordinates link" /> + +