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 + "/") }