From 4815e93fc998bf88794d21d1e05a5480d9c2d91b Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Thu, 31 May 2018 06:30:17 +0530 Subject: [PATCH] Make P18 edits to corresponding wikidata entity on uploading from Nearby (#1495) * Localisation updates from https://translatewiki.net. * Integrate API for displaying featured images (#1456) * Integrate API for displaying featured images * Add pagination and refactor code so that it can be reused for category images * Add license info to the images * Fix author view * Remove unused values * Fix minor issues with featured images * Fix null license url issue * Remove some log lines * Fix back navigation issue * fix tests * fix test inits * Gracefully handling various error situations * Added java docs * Update pull_request_template.md (#1476) * Update pull_request_template.md * Remove Javadocs mention * Added required/optional notes * resolves #1464 : MediaDataExtractor is making inefficient (redundant) server calls (#1496) * Open map of place where picture was taken (#1360) * Intent to map added * Merge conflicts resolved * Added the functionality to hide FAB incase of null coordinate * Merge Conflict resolved * Improve pr quality * Improve Quality * Added nested FAB animations * Nested FAB implemented * Improve Quality * Added up arrow * Javadocs Added * Add nearby tutorial (#1467) * Add dependency for MaterialShowcase * Add actionview class to get a reference to material showcase * Create a NearbyMaterialShowcaseSequence class * Apply sequence steps * Add first three steps of nearby showcase * Add sequence id constants to make sure they will be displayed only once * Add last step of sequence to explain plus fab * Create an object to prevent customize all sequences every time * Fix typo * Code cleanup * Add strings to strings.xml * Code cleanup * Revert irrelevant change * Revert irrelevant change * Remove showcaseview for recenter button * Use single showcaseView instead of sequence * Add single showcase view insted of sequence to be able to edit text style * Make sure it will be displayed only once * Cleanup * Update strings * Change dismiss text style * CONTRIBUTING: fix formatting of the gist of the guidelines (#1453) * CONTRIBUTING: fix formatting of the gist of the guidelines First level headings for a gist seems to be overkill. So, replace first level headings with an ordered-list which sounds more meaningful. * CONTRIBUTING: specify clearly that 'blame' is a feature of "Git" The contributing file specifies about the ability to know who wrote something without the need of @author javadoc tags but incorrectly attributes the feature to GitHub. Correctly attribute the feature to where it belongs, Git, and specify the name of the feature to help users easily take advantage of it. * Feature/switch to butterknife (#1494) * Implemented butterknife in MediaDetailFragment [issue #1491] * Implemented butterknife in MediaDetailPagerFragment [[issue #1491]] * post merge upstream master wip [[issue #1491]] * Localisation updates from https://translatewiki.net. * Bug fix #1504 (#1506) * Bug fix #1504 * Filtered messages with ConnectException [issue #1504] * A generalised message for exceptions in Nearby Activity [issue #1504] * Localisation updates from https://translatewiki.net. * Fix security exception crash while accessing network location provider (#1498) * Fix security exception crash while accessing network location provider * Added java docs * Localisation updates from https://translatewiki.net. * Log P18 edits to wikidata corresponding wikidata entity on uploading a nearby image * Added java docs * Fix test build * Refresh nearby * Refresh nearby list on successful edit * Java docs * Make authenticated wikidata edits * Updated toast message to show entity name that was edited --- app/build.gradle | 2 + .../main/java/fr/free/nrw/commons/Utils.java | 1 + .../commons/contributions/Contribution.java | 14 ++++ .../contributions/ContributionController.java | 12 ++- .../ContributionsListFragment.java | 2 +- .../commons/di/CommonsApplicationModule.java | 10 ++- .../location/LocationServiceManager.java | 3 +- .../mwapi/ApacheHttpClientMediaWikiApi.java | 74 +++++++++++++++++++ .../free/nrw/commons/mwapi/MediaWikiApi.java | 7 ++ .../nrw/commons/nearby/NearbyActivity.java | 45 +++++++---- .../commons/nearby/NearbyListFragment.java | 11 ++- .../nrw/commons/nearby/NearbyMapFragment.java | 3 +- .../fr/free/nrw/commons/nearby/Place.java | 15 ++++ .../notification/NotificationActivity.java | 5 +- .../nrw/commons/upload/ShareActivity.java | 13 ++-- .../nrw/commons/upload/UploadController.java | 3 +- .../nrw/commons/upload/UploadService.java | 52 +++++++++++++ .../wikidata/WikidataEditListener.java | 16 ++++ .../wikidata/WikidataEditListenerImpl.java | 20 +++++ app/src/main/res/values/strings.xml | 2 + .../mwapi/ApacheHttpClientMediaWikiApiTest.kt | 4 +- 21 files changed, 276 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java diff --git a/app/build.gradle b/app/build.gradle index 5d37f8f54..72dbd7546 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -130,6 +130,7 @@ android { productFlavors { prod { buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"" + buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"" buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"" @@ -146,6 +147,7 @@ android { beta { // What values do we need to hit the BETA versions of the site / api ? buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\"" + buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"" buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"" diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 91c23ce26..9f89586c0 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -178,6 +178,7 @@ public class Utils { } public static void handleWebUrl(Context context, Uri url) { + Timber.d("Launching web url %s", url.toString()); Intent browserIntent = new Intent(Intent.ACTION_VIEW, url); if (browserIntent.resolveActivity(context.getPackageManager()) == null) { Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index 7861f96de..99009c029 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -45,6 +45,7 @@ public class Contribution extends Media { private long transferred; private String decimalCoords; private boolean isMultiple; + private String wikiDataEntityId; public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp, int state, long dataLength, Date dateUploaded, long transferred, @@ -222,4 +223,17 @@ public class Contribution extends Media { throw new RuntimeException("Unrecognized license value: " + license); } + + public String getWikiDataEntityId() { + return wikiDataEntityId; + } + + /** + * When the corresponding wikidata entity is known as in case of nearby uploads, it can be set + * using the setter method + * @param wikiDataEntityId + */ + public void setWikiDataEntityId(String wikiDataEntityId) { + this.wikiDataEntityId = wikiDataEntityId; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 37b3d5377..ed6001f94 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -90,7 +90,7 @@ public class ContributionController { fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY); } - public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload) { + public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload, String wikiDataEntityId) { FragmentActivity activity = fragment.getActivity(); Timber.d("handleImagePicked() called with onActivityResult()"); Intent shareIntent = new Intent(activity, ShareActivity.class); @@ -102,9 +102,6 @@ public class ContributionController { shareIntent.setType(activity.getContentResolver().getType(imageData)); shareIntent.putExtra(EXTRA_STREAM, imageData); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY); - if (isDirectUpload) { - shareIntent.putExtra("isDirectUpload", true); - } break; case SELECT_FROM_CAMERA: //FIXME: Find out appropriate mime type @@ -113,9 +110,6 @@ public class ContributionController { shareIntent.setType("image/jpeg"); shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA); - if (isDirectUpload) { - shareIntent.putExtra("isDirectUpload", true); - } break; default: @@ -123,6 +117,10 @@ public class ContributionController { } Timber.i("Image selected"); try { + shareIntent.putExtra("isDirectUpload", isDirectUpload); + if (wikiDataEntityId != null && !wikiDataEntityId.equals("")) { + shareIntent.putExtra("wikiDataEntityId", wikiDataEntityId); + } activity.startActivity(shareIntent); } catch (SecurityException e) { Timber.e(e, "Security Exception"); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index ff400a8dd..0b600c5d0 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -117,7 +117,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); - controller.handleImagePicked(requestCode, data, false); + controller.handleImagePicked(requestCode, data, false, null); } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index 55281be7e..1c54366af 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -24,6 +24,8 @@ import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.nearby.NearbyPlaces; import fr.free.nrw.commons.upload.UploadController; +import fr.free.nrw.commons.wikidata.WikidataEditListener; +import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; import static android.content.Context.MODE_PRIVATE; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY; @@ -123,7 +125,7 @@ public class CommonsApplicationModule { @Named("default_preferences") SharedPreferences defaultPreferences, @Named("category_prefs") SharedPreferences categoryPrefs, Gson gson) { - return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, defaultPreferences, categoryPrefs, gson); + return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultPreferences, categoryPrefs, gson); } @Provides @@ -165,4 +167,10 @@ public class CommonsApplicationModule { public LruCache provideLruCache() { return new LruCache<>(1024); } + + @Provides + @Singleton + public WikidataEditListener provideWikidataEditListener() { + return new WikidataEditListenerImpl(); + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index 49c422633..cd1082ba5 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -284,6 +284,7 @@ public class LocationServiceManager implements LocationListener { LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving LOCATION_NOT_CHANGED, - PERMISSION_JUST_GRANTED + PERMISSION_JUST_GRANTED, + MAP_UPDATED } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 6629d0933..47b9b5925 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -25,6 +25,8 @@ import org.apache.http.params.CoreProtocolPNames; import org.apache.http.util.EntityUtils; import org.mediawiki.api.ApiResult; import org.mediawiki.api.MWApi; +import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.IOException; @@ -62,6 +64,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { private static final String THUMB_SIZE = "640"; private AbstractHttpClient httpClient; private MWApi api; + private MWApi wikidataApi; private Context context; private SharedPreferences defaultPreferences; private SharedPreferences categoryPreferences; @@ -69,6 +72,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { public ApacheHttpClientMediaWikiApi(Context context, String apiURL, + String wikidatApiURL, SharedPreferences defaultPreferences, SharedPreferences categoryPreferences, Gson gson) { @@ -82,6 +86,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent()); httpClient = new DefaultHttpClient(cm, params); api = new MWApi(apiURL, httpClient); + wikidataApi = new MWApi(wikidatApiURL, httpClient); this.defaultPreferences = defaultPreferences; this.categoryPreferences = categoryPreferences; this.gson = gson; @@ -206,6 +211,15 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { return api.getEditToken(); } + @Override + public String getCentralAuthToken() throws IOException { + String centralAuthToken = api.action("centralauthtoken") + .get() + .getString("/api/centralauthtoken/@centralauthtoken"); + Timber.d("MediaWiki Central auth token is %s", centralAuthToken); + return centralAuthToken; + } + @Override public boolean fileExistsWithName(String fileName) throws IOException { return api.action("query") @@ -351,6 +365,65 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { }).flatMapObservable(Observable::fromIterable); } + /** + * Get the edit token for making wiki data edits + * https://www.mediawiki.org/wiki/API:Tokens + * @return + * @throws IOException + */ + private String getWikidataEditToken() throws IOException { + return wikidataApi.getEditToken(); + } + + @Override + public String getWikidataCsrfToken() throws IOException { + String wikidataCsrfToken = wikidataApi.action("query") + .param("action", "query") + .param("centralauthtoken", getCentralAuthToken()) + .param("meta", "tokens") + .post() + .getString("/api/query/tokens/@csrftoken"); + Timber.d("Wikidata csrf token is %s", wikidataCsrfToken); + return wikidataCsrfToken; + } + + /** + * Creates a new claim using the wikidata API + * https://www.mediawiki.org/wiki/Wikibase/API + * @param entityId the wikidata entity to be edited + * @param property the property to be edited, for eg P18 for images + * @param snaktype the type of value stored for that property + * @param value the actual value to be stored for the property, for eg filename in case of P18 + * @return returns true if the claim is successfully created + * @throws IOException + */ + @Nullable + @Override + public boolean wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException { + ApiResult result = wikidataApi.action("wbcreateclaim") + .param("entity", entityId) + .param("centralauthtoken", getCentralAuthToken()) + .param("token", getWikidataCsrfToken()) + .param("snaktype", snaktype) + .param("property", property) + .param("value", value) + .post(); + + if (result == null || result.getNode("api") == null) { + return false; + } + + Node node = result.getNode("api").getDocument(); + Element element = (Element) node; + + if (element != null && element.getAttribute("success").equals("1")) { + return true; + } else { + Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info")); + } + return false; + } + @Override @NonNull public Observable searchTitles(String title, int searchCatsLimit) { @@ -586,6 +659,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { String resultStatus = result.getString("/api/upload/@result"); if (!resultStatus.equals("Success")) { String errorCode = result.getString("/api/error/@code"); + Timber.e(errorCode); return new UploadResult(resultStatus, errorCode); } else { Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index c0bd2fd87..869205f79 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -27,6 +27,10 @@ public interface MediaWikiApi { String getEditToken() throws IOException; + String getWikidataCsrfToken() throws IOException; + + String getCentralAuthToken() throws IOException; + boolean fileExistsWithName(String fileName) throws IOException; boolean pageExists(String pageName) throws IOException; @@ -49,6 +53,9 @@ public interface MediaWikiApi { @Nullable String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException; + @Nullable + boolean wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException; + @NonNull MediaResult fetchMediaByFilename(String filename) throws IOException; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java index 35e15b0d9..df31a8761 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java @@ -42,11 +42,13 @@ import butterknife.ButterKnife; import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager; +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.UriSerializer; 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.disposables.Disposable; @@ -55,8 +57,12 @@ import timber.log.Timber; import uk.co.deanwild.materialshowcaseview.IShowcaseListener; import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.*; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED; -public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener { + +public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener, + WikidataEditListener.WikidataP18EditListener { private static final int LOCATION_REQUEST = 1; @@ -76,6 +82,8 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp LocationServiceManager locationManager; @Inject NearbyController nearbyController; + @Inject WikidataEditListener wikidataEditListener; + @Inject @Named("application_preferences") SharedPreferences applicationPrefs; private LatLng curLatLng; @@ -110,6 +118,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp initBottomSheetBehaviour(); initDrawer(); + wikidataEditListener.setAuthenticationStateListener(this); } private void resumeFragment() { @@ -219,7 +228,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp //Still need to check if GPS is enabled checkGps(); lastKnownLocation = locationManager.getLKL(); - refreshView(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED); + refreshView(PERMISSION_JUST_GRANTED); } else { //If permission not granted, go to page that says Nearby Places cannot be displayed hideProgressBar(); @@ -279,7 +288,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp private void checkLocationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (locationManager.isLocationPermissionGranted()) { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } else { // Should we show an explanation? if (locationManager.isPermissionExplanationRequired(this)) { @@ -305,7 +314,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } } } else { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } } @@ -314,7 +323,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp super.onActivityResult(requestCode, resultCode, data); if (requestCode == 1) { Timber.d("User is back from Settings page"); - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } } @@ -373,8 +382,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp @Override public void onReceive(Context context, Intent intent) { if (NetworkUtils.isInternetConnectionEstablished(NearbyActivity.this)) { - refreshView(LocationServiceManager - .LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } else { ViewUtil.showLongToast(NearbyActivity.this, getString(R.string.no_internet)); } @@ -390,7 +398,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp * * @param locationChangeType defines if location shanged significantly or slightly */ - private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) { + private void refreshView(LocationChangeType locationChangeType) { if (lockNearbyView) { return; } @@ -403,12 +411,13 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp registerLocationUpdates(); LatLng lastLocation = locationManager.getLastLocation(); - if (curLatLng != null && curLatLng.equals(lastLocation)) { //refresh view only if location has changed + if (curLatLng != null && curLatLng.equals(lastLocation) + && !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed return; } curLatLng = lastLocation; - if (locationChangeType.equals(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED)) { + if (locationChangeType.equals(PERMISSION_JUST_GRANTED)) { curLatLng = lastKnownLocation; } @@ -417,8 +426,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp return; } - if (locationChangeType.equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) - || locationChangeType.equals(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED)) { + if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED) + || locationChangeType.equals(PERMISSION_JUST_GRANTED) + || locationChangeType.equals(MAP_UPDATED)) { progressBar.setVisibility(View.VISIBLE); //TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found @@ -440,7 +450,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp progressBar.setVisibility(View.GONE); }); } else if (locationChangeType - .equals(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) { + .equals(LOCATION_SLIGHTLY_CHANGED)) { Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriSerializer()) .create(); @@ -685,12 +695,12 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp @Override public void onLocationChangedSignificantly(LatLng latLng) { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } @Override public void onLocationChangedSlightly(LatLng latLng) { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED); + refreshView(LOCATION_SLIGHTLY_CHANGED); } public void prepareViewsForSheetPosition(int bottomSheetState) { @@ -700,4 +710,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp private void showErrorMessage(String message) { ViewUtil.showLongToast(NearbyActivity.this, message); } + + @Override + public void onWikidataEditSuccessful() { + refreshView(MAP_UPDATED); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index 1be2a8689..099792bc5 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java @@ -2,6 +2,7 @@ package fr.free.nrw.commons.nearby; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; @@ -21,6 +22,9 @@ import java.lang.reflect.Type; import java.util.Collections; import java.util.List; +import javax.inject.Inject; +import javax.inject.Named; + import dagger.android.support.AndroidSupportInjection; import dagger.android.support.DaggerFragment; import fr.free.nrw.commons.R; @@ -47,6 +51,11 @@ public class NearbyListFragment extends DaggerFragment { private RecyclerView recyclerView; private ContributionController controller; + + @Inject + @Named("direct_nearby_upload_prefs") + SharedPreferences directPrefs; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -137,7 +146,7 @@ public class NearbyListFragment extends DaggerFragment { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); - controller.handleImagePicked(requestCode, data, true); + controller.handleImagePicked(requestCode, data, true, directPrefs.getString("WikiDataEntityId", null)); } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index 69041d286..934d74353 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -731,6 +731,7 @@ public class NearbyMapFragment extends DaggerFragment { editor.putString("Title", place.getName()); editor.putString("Desc", place.getLongDescription()); editor.putString("Category", place.getCategory()); + editor.putString("WikiDataEntityId", place.getWikiDataEntityId()); editor.apply(); } @@ -766,7 +767,7 @@ public class NearbyMapFragment extends DaggerFragment { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); - controller.handleImagePicked(requestCode, data, true); + controller.handleImagePicked(requestCode, data, true, directPrefs.getString("WikiDataEntityId", null)); } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index 9c5138245..93075e8fe 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -3,6 +3,7 @@ package fr.free.nrw.commons.nearby; import android.graphics.Bitmap; import android.net.Uri; import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; import java.util.HashMap; import java.util.Map; @@ -50,6 +51,20 @@ public class Place { this.distance = distance; } + /** + * Extracts the entity id from the wikidata link + * @return returns the entity id if wikidata link exists + */ + @Nullable + public String getWikiDataEntityId() { + if (!hasWikidataLink()) { + return null; + } + + String wikiDataLink = siteLinks.getWikidataLink().toString(); + return wikiDataLink.replace("http://www.wikidata.org/entity/", ""); + } + public boolean hasWikipediaLink() { return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikipediaLink())); } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index dc52f198a..b366c944a 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -16,7 +16,6 @@ import android.widget.RelativeLayout; import com.pedrogomez.renderers.RVRendererAdapter; -import java.lang.ref.WeakReference; import java.util.Collections; import java.util.List; @@ -26,6 +25,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.ViewUtil; @@ -46,6 +46,8 @@ public class NotificationActivity extends NavigationBaseActivity { @BindView(R.id.container) RelativeLayout relativeLayout; @Inject NotificationController controller; + @Inject + MediaWikiApi mediaWikiApi; private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment"; private NotificationWorkerFragment mNotificationWorkerFragment; @@ -81,7 +83,6 @@ public class NotificationActivity extends NavigationBaseActivity { } } - @SuppressLint("CheckResult") private void addNotifications() { Timber.d("Add notifications"); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java index 6a59c8e30..40e36e0e0 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -8,7 +8,6 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.ContentResolver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -69,14 +68,11 @@ import fr.free.nrw.commons.caching.CacheController; import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.OnCategoriesSaveHandler; import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.TemplateRemoveModifier; - -import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; @@ -135,6 +131,7 @@ public class ShareActivity private String title; private String description; + private String wikiDataEntityId; private Snackbar snackbar; private boolean duplicateCheckPassed = false; @@ -194,7 +191,7 @@ public class ShareActivity Timber.d("Cache the categories found"); } - uploadController.startUpload(title, mediaUri, description, mimeType, source, decimalCoords, c -> { + uploadController.startUpload(title, mediaUri, description, mimeType, source, decimalCoords, wikiDataEntityId, c -> { ShareActivity.this.contribution = c; showPostUpload(); }); @@ -278,7 +275,10 @@ public class ShareActivity } if (intent.hasExtra("isDirectUpload")) { Timber.d("This was initiated by a direct upload from Nearby"); - isNearbyUpload = true; + isNearbyUpload = intent.getBooleanExtra("isDirectUpload", false); + } + if (intent.hasExtra("wikiDataEntityId")) { + wikiDataEntityId = intent.getStringExtra("wikiDataEntityId"); } mimeType = intent.getType(); } @@ -446,7 +446,6 @@ public class ShareActivity }); } - @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index 32554da0f..5fd6d909b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -91,7 +91,7 @@ public class UploadController { * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615") * @param onComplete the progress tracker */ - public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) { + public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) { Contribution contribution; //TODO: Modify this to include coords @@ -101,6 +101,7 @@ public class UploadController { contribution.setTag("mimeType", mimeType); contribution.setSource(source); + contribution.setWikiDataEntityId(wikiDataEntityId); //Calls the next overloaded method startUpload(contribution, onComplete); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 94c005256..39d53d2f0 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -18,6 +18,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.HashSet; +import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,6 +37,11 @@ import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.UploadResult; +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; public class UploadService extends HandlerService { @@ -49,9 +55,11 @@ public class UploadService extends HandlerService { public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign"; @Inject MediaWikiApi mwApi; + @Inject WikidataEditListener wikidataEditListener; @Inject SessionManager sessionManager; @Inject @Named("default_preferences") SharedPreferences prefs; @Inject ContributionDao contributionDao; + @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; private NotificationManager notificationManager; private NotificationCompat.Builder curProgressNotification; @@ -137,6 +145,7 @@ public class UploadService extends HandlerService { @Override public void queue(int what, Contribution contribution) { + Timber.d("Upload service queue has contribution with wiki data entity id as %s", contribution.getWikiDataEntityId()); switch (what) { case ACTION_UPLOAD_FILE: @@ -253,6 +262,7 @@ public class UploadService extends HandlerService { if (!resultStatus.equals("Success")) { showFailedNotification(contribution); } else { + editWikidataProperty(contribution); contribution.setFilename(uploadResult.getCanonicalFilename()); contribution.setImageUrl(uploadResult.getImageUrl()); contribution.setState(Contribution.STATE_COMPLETED); @@ -275,6 +285,48 @@ public class UploadService extends HandlerService { } } + /** + * Edits the wikidata entity by adding the P18 property to it. + * Adding the P18 edit requires calling the wikidata API to create a claim against the entity + * @param contribution + */ + @SuppressLint("CheckResult") + private void editWikidataProperty(Contribution contribution) { + Timber.d("Upload successful with wiki data entity id as %s", contribution.getWikiDataEntityId()); + Timber.d("Attempting to edit Wikidata property %s", contribution.getWikiDataEntityId()); + Observable.fromCallable(() -> mwApi.wikidatCreateClaim(contribution.getWikiDataEntityId(), "P18", "value", getFileName(contribution))) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> handleClaimResult(contribution, result), throwable -> { + Timber.e(throwable, "Error occurred while making claim"); + ViewUtil.showLongToast(getBaseContext(), getResources().getString(R.string.wikidata_edit_failure)); + }); + } + + private void handleClaimResult(Contribution contribution, Boolean result) { + if(result) { + wikidataEditListener.onSuccessfulWikidataEdit(); + String title = directPrefs.getString("Title", ""); + String successStringTemplate = getResources().getString(R.string.successful_wikidata_edit); + String successMessage = String.format(Locale.getDefault(), successStringTemplate, title); + ViewUtil.showLongToast(getBaseContext(), successMessage); + } else { + Timber.d("Unable to make wiki data edit for entity %s", contribution.getWikiDataEntityId()); + ViewUtil.showLongToast(getBaseContext(), getResources().getString(R.string.wikidata_edit_failure)); + } + } + + /** + * Formats and returns the filename as accepted by the wiki base API + * https://www.mediawiki.org/wiki/Wikibase/API#wbcreateclaim + * @param contribution + * @return + */ + private String getFileName(Contribution contribution) { + String filename = String.format("\"%s\"", contribution.getFilename().replace("File:", "")); + return filename; + } + @SuppressLint("StringFormatInvalid") private void showFailedNotification(Contribution contribution) { Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true) diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java new file mode 100644 index 000000000..30fb26ddc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java @@ -0,0 +1,16 @@ +package fr.free.nrw.commons.wikidata; + +public abstract class WikidataEditListener { + + protected WikidataP18EditListener wikidataP18EditListener; + + public abstract void onSuccessfulWikidataEdit(); + + public void setAuthenticationStateListener(WikidataP18EditListener wikidataP18EditListener) { + this.wikidataP18EditListener = wikidataP18EditListener; + } + + public interface WikidataP18EditListener { + void onWikidataEditSuccessful(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java new file mode 100644 index 000000000..407c24711 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.wikidata; + +/** + * Listener for wikidata edits + */ +public class WikidataEditListenerImpl extends WikidataEditListener { + + public WikidataEditListenerImpl() { + } + + /** + * Fired when wikidata P18 edit is successful. If there's an active listener, then it is fired + */ + @Override + public void onSuccessfulWikidataEdit() { + if (wikidataP18EditListener != null) { + wikidataP18EditListener.onWikidataEditSuccessful(); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6e30baa10..60a977f0b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -284,4 +284,6 @@ Coordinates were not specified during image selection Error fetching nearby places. + Image successfully added to %1$s on Wikidata! + Failed to update corresponding wiki data entity! diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt index 686a90ef2..85f1ed98e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt @@ -26,15 +26,17 @@ class ApacheHttpClientMediaWikiApiTest { private lateinit var testObject: ApacheHttpClientMediaWikiApi private lateinit var server: MockWebServer + private lateinit var wikidataServer: MockWebServer private lateinit var sharedPreferences: SharedPreferences private lateinit var categoryPreferences: SharedPreferences @Before fun setUp() { server = MockWebServer() + wikidataServer = MockWebServer() sharedPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application) categoryPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application) - testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", sharedPreferences, categoryPreferences, Gson()) + testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", "http://" + wikidataServer.hostName + ":" + wikidataServer.port + "/", sharedPreferences, categoryPreferences, Gson()) testObject.setWikiMediaToolforgeUrl("http://" + server.hostName + ":" + server.port + "/") }