Use JSON SPARQL query for fetching nearby places (#2398)

* Use JSON response for nearby places

* Move okhttp calls to a different class

* wip

* Fetch picture of the day using JSON API

* Search images using JSON APIs

* tests

* Fix injection based on code review comments
This commit is contained in:
Vivek Maskara 2019-02-06 10:40:30 +05:30 committed by Ashish Kumar
parent 323527b3be
commit f12837650a
44 changed files with 1472 additions and 418 deletions

View file

@ -1,5 +1,7 @@
package fr.free.nrw.commons;
import android.content.Context;
/**
* Base presenter, enforcing contracts to atach and detach view
*/

View file

@ -14,6 +14,10 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.model.ImageInfo;
import fr.free.nrw.commons.media.model.MwQueryPage;
import fr.free.nrw.commons.utils.DateUtils;
import fr.free.nrw.commons.utils.StringUtils;
public class Media implements Parcelable {
@ -428,4 +432,24 @@ public class Media implements Parcelable {
public boolean getRequestedDeletion(){
return requestedDeletion;
}
public static Media from(MwQueryPage page) {
ImageInfo imageInfo = page.imageInfo();
if(imageInfo == null) {
return null;
}
Media media = new Media(null,
imageInfo.getOriginalUrl(),
page.title(),
imageInfo.getMetadata().imageDescription().value(),
0,
DateUtils.getDateFromString(imageInfo.getMetadata().getDateTimeOriginal().value()),
DateUtils.getDateFromString(imageInfo.getMetadata().getDateTime().value()),
StringUtils.getParsedStringFromHtml(imageInfo.getMetadata().getArtist().value())
);
media.setLicense(imageInfo.getMetadata().getLicenseShortName().value());
return media;
}
}

View file

@ -43,6 +43,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Single;
@ -100,7 +101,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
@Inject
SessionManager sessionManager;
@Inject
MediaWikiApi mediaWikiApi;
OkHttpJsonApiClient okHttpJsonApiClient;
MenuItem item;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@ -201,7 +202,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
if (checkAccount()) {
try{
compositeDisposable.add(mediaWikiApi
compositeDisposable.add(okHttpJsonApiClient
.getAchievements(Objects.requireNonNull(sessionManager.getCurrentAccount()).name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -247,7 +248,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
*/
private void setUploadCount(Achievements achievements) {
if (checkAccount()) {
compositeDisposable.add(mediaWikiApi
compositeDisposable.add(okHttpJsonApiClient
.getUploadCount(Objects.requireNonNull(sessionManager.getCurrentAccount()).name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())

View file

@ -8,20 +8,21 @@ import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.bookmarks.Bookmark;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
@Singleton
public class BookmarkPicturesController {
private MediaWikiApi mediaWikiApi;
@Inject
BookmarkPicturesDao bookmarkDao;
private final OkHttpJsonApiClient okHttpJsonApiClient;
private final BookmarkPicturesDao bookmarkDao;
private List<Bookmark> currentBookmarks;
@Inject public BookmarkPicturesController(MediaWikiApi mediaWikiApi) {
this.mediaWikiApi = mediaWikiApi;
@Inject
public BookmarkPicturesController(OkHttpJsonApiClient okHttpJsonApiClient,
BookmarkPicturesDao bookmarkDao) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.bookmarkDao = bookmarkDao;
currentBookmarks = new ArrayList<>();
}
@ -34,7 +35,9 @@ public class BookmarkPicturesController {
currentBookmarks = bookmarks;
ArrayList<Media> medias = new ArrayList<>();
for (Bookmark bookmark : bookmarks) {
List<Media> tmpMedias = mediaWikiApi.searchImages(bookmark.getMediaName(), 0);
List<Media> tmpMedias = okHttpJsonApiClient
.searchImages(bookmark.getMediaName(), 0)
.blockingGet();
for (Media m : tmpMedias) {
if (m.getCreator().trim().equals(bookmark.getMediaCreator().trim())) {
medias.add(m);

View file

@ -13,11 +13,13 @@ import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import fr.free.nrw.commons.bookmarks.Bookmark;
import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.BASE_URI;
@Singleton
public class BookmarkPicturesDao {
private final Provider<ContentProviderClient> clientProvider;

View file

@ -1,5 +1,7 @@
package fr.free.nrw.commons.campaigns;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import java.text.ParseException;
@ -8,9 +10,13 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.MvpView;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -21,16 +27,22 @@ import io.reactivex.schedulers.Schedulers;
* The presenter for the campaigns view, fetches the campaigns from the api and informs the view on
* success and error
*/
@Singleton
public class CampaignsPresenter implements BasePresenter {
private final OkHttpJsonApiClient okHttpJsonApiClient;
private final String TAG = "#CampaignsPresenter#";
private ICampaignsView view;
private MediaWikiApi mediaWikiApi;
private Disposable disposable;
private Campaign campaign;
@Inject
public CampaignsPresenter(OkHttpJsonApiClient okHttpJsonApiClient) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
}
@Override public void onAttachView(MvpView view) {
this.view = (ICampaignsView) view;
this.mediaWikiApi = ((ICampaignsView) view).getMediaWikiApi();
}
@Override public void onDetachView() {
@ -43,14 +55,15 @@ public class CampaignsPresenter implements BasePresenter {
/**
* make the api call to fetch the campaigns
*/
@SuppressLint("CheckResult")
public void getCampaigns() {
if (view != null && mediaWikiApi != null) {
if (view != null && okHttpJsonApiClient != null) {
//If we already have a campaign, lets not make another call
if (this.campaign != null) {
view.showCampaigns(campaign);
return;
}
Single<CampaignResponseDTO> campaigns = mediaWikiApi.getCampaigns();
Single<CampaignResponseDTO> campaigns = okHttpJsonApiClient.getCampaigns();
campaigns.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribeWith(new SingleObserver<CampaignResponseDTO>() {

View file

@ -7,7 +7,5 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
* Interface which defines the view contracts of the campaign view
*/
public interface ICampaignsView extends MvpView {
MediaWikiApi getMediaWikiApi();
void showCampaigns(Campaign campaign);
}

View file

@ -51,10 +51,10 @@ import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.nearby.NearbyNotificationCardView;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadService;
import fr.free.nrw.commons.utils.ConfigUtils;
@ -80,12 +80,14 @@ public class ContributionsFragment
MediaDetailPagerFragment.MediaDetailProvider,
FragmentManager.OnBackStackChangedListener,
ContributionsListFragment.SourceRefresher,
LocationUpdateListener,ICampaignsView
{
LocationUpdateListener,
ICampaignsView {
@Inject @Named("default_preferences") BasicKvStore defaultKvStore;
@Inject ContributionDao contributionDao;
@Inject MediaWikiApi mediaWikiApi;
@Inject NearbyController nearbyController;
@Inject NearbyController nearbyController;
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
@Inject CampaignsPresenter presenter;
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
private UploadService uploadService;
@ -109,7 +111,6 @@ public class ContributionsFragment
private boolean isFragmentAttachedBefore = false;
private View checkBoxView;
private CheckBox checkBox;
private CampaignsPresenter presenter;
/**
* Since we will need to use parent activity on onAuthCookieAcquired, we have to wait
@ -144,7 +145,6 @@ public class ContributionsFragment
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contributions, container, false);
ButterKnife.bind(this, view);
presenter = new CampaignsPresenter();
presenter.onAttachView(this);
campaignView.setVisibility(View.GONE);
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
@ -451,7 +451,7 @@ public class ContributionsFragment
@SuppressWarnings("ConstantConditions")
private void setUploadCount() {
compositeDisposable.add(mediaWikiApi
compositeDisposable.add(okHttpJsonApiClient
.getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -695,10 +695,6 @@ public class ContributionsFragment
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
}
@Override public MediaWikiApi getMediaWikiApi() {
return mediaWikiApi;
}
@Override public void showCampaigns(Campaign campaign) {
if (campaign != null) {
campaignView.setCampaign(campaign);

View file

@ -2,50 +2,37 @@ package fr.free.nrw.commons.delete;
import android.accounts.Account;
import android.content.Context;
import android.util.Log;
import com.google.gson.JsonObject;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.Single;
import timber.log.Timber;
@Singleton
public class ReasonBuilder {
private SessionManager sessionManager;
private MediaWikiApi mediaWikiApi;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private String reason;
private OkHttpJsonApiClient okHttpJsonApiClient;
private Context context;
private Media media;
public ReasonBuilder(String reason,
Context context,
Media media,
@Inject
public ReasonBuilder(Context context,
SessionManager sessionManager,
MediaWikiApi mediaWikiApi){
this.reason = reason;
OkHttpJsonApiClient okHttpJsonApiClient) {
this.context = context;
this.media = media;
this.sessionManager = sessionManager;
this.mediaWikiApi = mediaWikiApi;
this.okHttpJsonApiClient = okHttpJsonApiClient;
}
private String prettyUploadedDate(Media media) {
@ -57,31 +44,27 @@ public class ReasonBuilder {
return formatter.format(date);
}
private void fetchArticleNumber() {
private Single<String> fetchArticleNumber(Media media, String reason) {
if (checkAccount()) {
compositeDisposable.add(mediaWikiApi
return okHttpJsonApiClient
.getAchievements(sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
this::appendArticlesUsed,
t -> Timber.e(t, "Fetching achievements statistics failed")
));
.map(feedbackResponse -> appendArticlesUsed(feedbackResponse, media, reason));
}
return Single.just("");
}
private void appendArticlesUsed(FeedbackResponse object){
reason += context.getString(R.string.uploaded_by_myself).toString() + prettyUploadedDate(media);
reason += context.getString(R.string.used_by).toString()
private String appendArticlesUsed(FeedbackResponse object, Media media, String reason) {
reason += context.getString(R.string.uploaded_by_myself) + prettyUploadedDate(media);
reason += context.getString(R.string.used_by)
+ object.getArticlesUsingImages()
+ context.getString(R.string.articles).toString();
Log.i("New Reason", reason);
+ context.getString(R.string.articles);
Timber.i("New Reason %s", reason);
return reason;
}
public String getReason(){
fetchArticleNumber();
return reason;
public Single<String> getReason(Media media, String reason) {
return fetchArticleNumber(media, reason);
}
/**

View file

@ -9,6 +9,7 @@ import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.campaigns.CampaignsPresenter;
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
import fr.free.nrw.commons.delete.DeleteTask;
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;

View file

@ -191,12 +191,6 @@ public class CommonsApplicationModule {
return new DBOpenHelper(context);
}
@Provides
@Singleton
public NearbyPlaces provideNearbyPlaces() {
return new NearbyPlaces();
}
@Provides
@Singleton
public LruCache<String, String> provideLruCache() {

View file

@ -19,36 +19,68 @@ import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.utils.UriDeserializer;
import fr.free.nrw.commons.utils.UriSerializer;
import okhttp3.Cache;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import timber.log.Timber;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public class NetworkingModule {
private static final String WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql";
private final String WIKIMEDIA_CAMPAIGNS_BASE_URL =
"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json";
private static final String TOOLS_FORGE_URL = "https://tools.wmflabs.org/";
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
@Provides
@Singleton
public OkHttpClient provideOkHttpClient(Context context) {
public OkHttpClient provideOkHttpClient(Context context,
HttpLoggingInterceptor httpLoggingInterceptor) {
File dir = new File(context.getCacheDir(), "okHttpCache");
return new OkHttpClient.Builder().connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.addInterceptor(httpLoggingInterceptor)
.readTimeout(60, TimeUnit.SECONDS)
.cache(new Cache(dir, OK_HTTP_CACHE_SIZE))
.build();
}
@Provides
@Singleton
public HttpLoggingInterceptor provideHttpLoggingInterceptor() {
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(message -> {
Timber.tag("OkHttp").d(message);
});
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return httpLoggingInterceptor;
}
@Provides
@Singleton
public MediaWikiApi provideMediaWikiApi(Context context,
@Named("default_preferences") BasicKvStore defaultKvStore,
@Named("category_prefs") BasicKvStore categoryKvStore,
Gson gson,
OkHttpClient okHttpClient) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultKvStore, categoryKvStore, gson, okHttpClient);
Gson gson) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultKvStore, categoryKvStore, gson);
}
@Provides
@Singleton
public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient,
@Named("tools_force") HttpUrl toolsForgeUrl,
Gson gson) {
return new OkHttpJsonApiClient(okHttpClient,
toolsForgeUrl,
WIKIDATA_SPARQL_QUERY_URL,
WIKIMEDIA_CAMPAIGNS_BASE_URL,
BuildConfig.WIKIMEDIA_API_HOST,
gson);
}
@Provides
@ -59,6 +91,14 @@ public class NetworkingModule {
return HttpUrl.parse(BuildConfig.COMMONS_URL);
}
@Provides
@Named("tools_force")
@NonNull
@SuppressWarnings("ConstantConditions")
public HttpUrl provideToolsForgeUrl() {
return HttpUrl.parse(TOOLS_FORGE_URL);
}
/**
* Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
* @return returns a singleton Gson instance

View file

@ -33,10 +33,9 @@ import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
@ -63,7 +62,8 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
ProgressBar bottomProgressBar;
@Inject RecentSearchesDao recentSearchesDao;
@Inject MediaWikiApi mwApi;
@Inject
OkHttpJsonApiClient okHttpJsonApiClient;
@Inject @Named("default_preferences") BasicKvStore defaultKvStore;
private RVRendererAdapter<Media> imagesAdapter;
@ -140,7 +140,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
bottomProgressBar.setVisibility(GONE);
queryList.clear();
imagesAdapter.clear();
Observable.fromCallable(() -> mwApi.searchImages(query,queryList.size()))
okHttpJsonApiClient.searchImages(query, queryList.size())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@ -156,7 +156,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
this.query = query;
bottomProgressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(GONE);
Observable.fromCallable(() -> mwApi.searchImages(query,queryList.size()))
okHttpJsonApiClient.searchImages(query, queryList.size())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)

View file

@ -0,0 +1,34 @@
package fr.free.nrw.commons.json;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
public class PostProcessingTypeAdapter implements TypeAdapterFactory {
public interface PostProcessable {
void postProcess();
}
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<T>() {
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
public T read(JsonReader in) throws IOException {
T obj = delegate.read(in);
if (obj instanceof PostProcessable) {
((PostProcessable)obj).postProcess();
}
return obj;
}
};
}
}

View file

@ -1,9 +1,9 @@
package fr.free.nrw.commons.media;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.DataSetObserver;
import android.net.Uri;
@ -12,7 +12,6 @@ import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.Html;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
@ -27,7 +26,6 @@ import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
@ -54,8 +52,11 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.ui.widget.CompatTextView;
import timber.log.Timber;
import fr.free.nrw.commons.utils.DateUtils;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static android.content.Context.CLIPBOARD_SERVICE;
import static android.view.View.GONE;
@ -93,6 +94,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
MediaWikiApi mwApi;
@Inject
SessionManager sessionManager;
@Inject
ReasonBuilder reasonBuilder;
private int initialListTop = 0;
@ -404,19 +407,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(spinner);
builder.setTitle(R.string.nominate_delete)
.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> {
String reason = spinner.getSelectedItem().toString();
ReasonBuilder reasonBuilder = new ReasonBuilder(reason,
getActivity(),
media,
sessionManager,
mwApi);
reason = reasonBuilder.getReason();
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
deleteTask.execute();
isDeleted = true;
enableDeleteButton(false);
});
.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> onDeleteClicked(spinner));
builder.setNegativeButton(R.string.about_translate_cancel, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
@ -425,6 +416,21 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
}
@SuppressLint("CheckResult")
private void onDeleteClicked(Spinner spinner) {
String reason = spinner.getSelectedItem().toString();
Single<String> deletionReason = reasonBuilder.getReason(media, reason);
deletionReason
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
deleteTask.execute();
isDeleted = true;
enableDeleteButton(false);
});
}
@OnClick(R.id.seeMore)
public void onSeeMoreClicked(){
if (nominatedForDeletion.getVisibility()== VISIBLE) {

View file

@ -0,0 +1,161 @@
package fr.free.nrw.commons.media.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
public class ExtMetadata {
@SuppressWarnings("unused") @SerializedName("DateTime") @Nullable
private Values dateTime;
@SuppressWarnings("unused") @SerializedName("ObjectName") @Nullable private Values objectName;
@SuppressWarnings("unused") @SerializedName("CommonsMetadataExtension") @Nullable private Values commonsMetadataExtension;
@SuppressWarnings("unused") @SerializedName("Categories") @Nullable private Values categories;
@SuppressWarnings("unused") @SerializedName("Assessments") @Nullable private Values assessments;
@SuppressWarnings("unused") @SerializedName("ImageDescription") @Nullable private Values imageDescription;
@SuppressWarnings("unused") @SerializedName("DateTimeOriginal") @Nullable private Values dateTimeOriginal;
@SuppressWarnings("unused") @SerializedName("Artist") @Nullable private Values artist;
@SuppressWarnings("unused") @SerializedName("Credit") @Nullable private Values credit;
@SuppressWarnings("unused") @SerializedName("Permission") @Nullable private Values permission;
@SuppressWarnings("unused") @SerializedName("AuthorCount") @Nullable private Values authorCount;
@SuppressWarnings("unused") @SerializedName("LicenseShortName") @Nullable private Values licenseShortName;
@SuppressWarnings("unused") @SerializedName("UsageTerms") @Nullable private Values usageTerms;
@SuppressWarnings("unused") @SerializedName("LicenseUrl") @Nullable private Values licenseUrl;
@SuppressWarnings("unused") @SerializedName("AttributionRequired") @Nullable private Values attributionRequired;
@SuppressWarnings("unused") @SerializedName("Copyrighted") @Nullable private Values copyrighted;
@SuppressWarnings("unused") @SerializedName("Restrictions") @Nullable private Values restrictions;
@SuppressWarnings("unused") @SerializedName("License") @Nullable private Values license;
@Nullable public Values licenseShortName() {
return licenseShortName;
}
@Nullable public Values licenseUrl() {
return licenseUrl;
}
@Nullable public Values license() {
return license;
}
@Nullable public Values imageDescription() {
return imageDescription;
}
@Nullable
public Values getDateTime() {
return dateTime;
}
@Nullable
public Values getObjectName() {
return objectName;
}
@Nullable
public Values getCommonsMetadataExtension() {
return commonsMetadataExtension;
}
@Nullable
public Values getCategories() {
return categories;
}
@Nullable
public Values getAssessments() {
return assessments;
}
@Nullable
public Values getImageDescription() {
return imageDescription;
}
@Nullable
public Values getDateTimeOriginal() {
return dateTimeOriginal;
}
@Nullable
public Values getArtist() {
return artist;
}
@Nullable
public Values getCredit() {
return credit;
}
@Nullable
public Values getPermission() {
return permission;
}
@Nullable
public Values getAuthorCount() {
return authorCount;
}
@Nullable
public Values getLicenseShortName() {
return licenseShortName;
}
@Nullable
public Values getUsageTerms() {
return usageTerms;
}
@Nullable
public Values getLicenseUrl() {
return licenseUrl;
}
@Nullable
public Values getAttributionRequired() {
return attributionRequired;
}
@Nullable
public Values getCopyrighted() {
return copyrighted;
}
@Nullable
public Values getRestrictions() {
return restrictions;
}
@Nullable
public Values getLicense() {
return license;
}
@Nullable public Values objectName() {
return objectName;
}
@Nullable public Values usageTerms() {
return usageTerms;
}
@Nullable public Values artist() {
return artist;
}
public class Values {
@SuppressWarnings("unused,NullableProblems") @NonNull
private String value;
@SuppressWarnings("unused,NullableProblems") @NonNull private String source;
@SuppressWarnings("unused,NullableProblems") @NonNull private String hidden;
@NonNull public String value() {
return value;
}
@NonNull public String source() {
return source;
}
}
}

View file

@ -0,0 +1,63 @@
package fr.free.nrw.commons.media.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import fr.free.nrw.commons.utils.StringUtils;
/**
* Gson POJO for a standard image info object as returned by the API ImageInfo module
*/
public class ImageInfo implements Serializable {
@SuppressWarnings("unused") private int size;
@SuppressWarnings("unused") private int width;
@SuppressWarnings("unused") private int height;
@SuppressWarnings("unused,NullableProblems") @Nullable
private String source;
@SuppressWarnings("unused") @SerializedName("thumburl") @Nullable private String thumbUrl;
@SuppressWarnings("unused") @SerializedName("thumbwidth") private int thumbWidth;
@SuppressWarnings("unused") @SerializedName("thumbheight") private int thumbHeight;
@SuppressWarnings("unused") @SerializedName("url") @Nullable private String originalUrl;
@SuppressWarnings("unused") @SerializedName("descriptionurl") @Nullable private String descriptionUrl;
@SuppressWarnings("unused") @SerializedName("descriptionshorturl") @Nullable private String descriptionShortUrl;
@SuppressWarnings("unused,NullableProblems") @SerializedName("mime") @NonNull
private String mimeType = "*/*";
@SuppressWarnings("unused") @SerializedName("extmetadata")@Nullable private ExtMetadata metadata;
@NonNull
public String getSource() {
return StringUtils.defaultString(source);
}
public void setSource(@Nullable String source) {
this.source = source;
}
public int getSize() {
return size;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
@NonNull public String getThumbUrl() {
return StringUtils.defaultString(thumbUrl);
}
@NonNull public String getOriginalUrl() {
return StringUtils.defaultString(originalUrl);
}
@Nullable public ExtMetadata getMetadata() {
return metadata;
}
}

View file

@ -0,0 +1,292 @@
package fr.free.nrw.commons.media.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.util.Collections;
import java.util.List;
import fr.free.nrw.commons.utils.StringUtils;
/**
* A class representing a standard page object as returned by the MediaWiki API.
*/
public class MwQueryPage {
@SuppressWarnings("unused")
private int pageid;
@SuppressWarnings("unused")
private int ns;
@SuppressWarnings("unused")
private int index;
@SuppressWarnings("unused,NullableProblems")
@NonNull
private String title;
@SuppressWarnings("unused")
@Nullable
private List<LangLink> langlinks;
@SuppressWarnings("unused")
@Nullable
private List<Revision> revisions;
@SuppressWarnings("unused")
@Nullable
private List<Coordinates> coordinates;
@SuppressWarnings("unused")
@Nullable
private List<Category> categories;
@SuppressWarnings("unused")
@Nullable
private PageProps pageprops;
@SuppressWarnings("unused")
@Nullable
private String extract;
@SuppressWarnings("unused")
@Nullable
private Thumbnail thumbnail;
@SuppressWarnings("unused")
@Nullable
private String description;
@SuppressWarnings("unused")
@SerializedName("descriptionsource")
@Nullable
private String descriptionSource;
@SuppressWarnings("unused")
@SerializedName("imageinfo")
@Nullable
private List<ImageInfo> imageInfo;
@Nullable
private String redirectFrom;
@Nullable
private String convertedFrom;
@Nullable
private String convertedTo;
@NonNull
public String title() {
return title;
}
public int index() {
return index;
}
@Nullable
public List<LangLink> langLinks() {
return langlinks;
}
@Nullable
public List<Revision> revisions() {
return revisions;
}
@Nullable
public List<Category> categories() {
return categories;
}
@Nullable
public List<Coordinates> coordinates() {
// TODO: Handle null values in lists during deserialization, perhaps with a new
// @RequiredElements annotation and corresponding TypeAdapter
if (coordinates != null) {
coordinates.removeAll(Collections.singleton(null));
}
return coordinates;
}
public int pageId() {
return pageid;
}
@Nullable
public PageProps pageProps() {
return pageprops;
}
@Nullable
public String extract() {
return extract;
}
@Nullable
public String thumbUrl() {
return thumbnail != null ? thumbnail.source() : null;
}
@Nullable
public String description() {
return description;
}
@Nullable
public String descriptionSource() {
return descriptionSource;
}
@Nullable
public ImageInfo imageInfo() {
return imageInfo != null ? imageInfo.get(0) : null;
}
@Nullable
public String redirectFrom() {
return redirectFrom;
}
public void redirectFrom(@Nullable String from) {
redirectFrom = from;
}
@Nullable
public String convertedFrom() {
return convertedFrom;
}
public void convertedFrom(@Nullable String from) {
convertedFrom = from;
}
@Nullable
public String convertedTo() {
return convertedTo;
}
public void convertedTo(@Nullable String to) {
convertedTo = to;
}
public void appendTitleFragment(@Nullable String fragment) {
title += "#" + fragment;
}
public static class Revision {
@SuppressWarnings("unused,NullableProblems")
@SerializedName("contentformat")
@NonNull
private String contentFormat;
@SuppressWarnings("unused,NullableProblems")
@SerializedName("contentmodel")
@NonNull
private String contentModel;
@SuppressWarnings("unused,NullableProblems")
@SerializedName("timestamp")
@NonNull
private String timeStamp;
@SuppressWarnings("unused,NullableProblems")
@NonNull
private String content;
@NonNull
public String content() {
return content;
}
@NonNull
public String timeStamp() {
return StringUtils.defaultString(timeStamp);
}
}
public static class LangLink {
@SuppressWarnings("unused,NullableProblems")
@NonNull
private String lang;
@SuppressWarnings("unused,NullableProblems")
@NonNull
private String title;
@NonNull
public String lang() {
return lang;
}
@NonNull
public String title() {
return title;
}
}
public static class Coordinates {
@SuppressWarnings("unused")
@Nullable
private Double lat;
@SuppressWarnings("unused")
@Nullable
private Double lon;
@Nullable
public Double lat() {
return lat;
}
@Nullable
public Double lon() {
return lon;
}
}
static class Thumbnail {
@SuppressWarnings("unused")
private String source;
@SuppressWarnings("unused")
private int width;
@SuppressWarnings("unused")
private int height;
String source() {
return source;
}
}
public static class PageProps {
@SuppressWarnings("unused")
@SerializedName("wikibase_item")
@Nullable
private String wikiBaseItem;
@SuppressWarnings("unused")
@Nullable
private String displaytitle;
@SuppressWarnings("unused")
@Nullable
private String disambiguation;
@Nullable
public String getDisplayTitle() {
return displaytitle;
}
@NonNull
public String getWikiBaseItem() {
return StringUtils.defaultString(wikiBaseItem);
}
public boolean isDisambiguation() {
return disambiguation != null;
}
}
public static class Category {
@SuppressWarnings("unused")
private int ns;
@SuppressWarnings("unused,NullableProblems")
@Nullable
private String title;
@SuppressWarnings("unused")
private boolean hidden;
public int ns() {
return ns;
}
@NonNull
public String title() {
return StringUtils.defaultString(title);
}
public boolean hidden() {
return hidden;
}
}
}

View file

@ -5,7 +5,6 @@ import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import com.google.gson.Gson;
@ -21,7 +20,6 @@ import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@ -41,12 +39,8 @@ import java.util.concurrent.Callable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.achievements.FeaturedImages;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.category.CategoryImageUtils;
import fr.free.nrw.commons.category.QueryContinue;
import fr.free.nrw.commons.kvstore.BasicKvStore;
@ -59,10 +53,6 @@ import fr.free.nrw.commons.utils.ViewUtil;
import in.yuvi.http.fluent.Http;
import io.reactivex.Observable;
import io.reactivex.Single;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue;
@ -71,8 +61,6 @@ import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue;
* @author Addshore
*/
public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private String wikiMediaToolforgeUrl = "https://tools.wmflabs.org/";
private static final String THUMB_SIZE = "640";
private AbstractHttpClient httpClient;
private CustomMwApi api;
@ -81,9 +69,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private BasicKvStore defaultKvStore;
private BasicKvStore categoryKvStore;
private Gson gson;
private final OkHttpClient okHttpClient;
private final String WIKIMEDIA_CAMPAIGNS_BASE_URL =
"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json";
private final String ERROR_CODE_BAD_TOKEN = "badtoken";
@ -92,10 +77,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
String wikidatApiURL,
BasicKvStore defaultKvStore,
BasicKvStore categoryKvStore,
Gson gson,
OkHttpClient okHttpClient) {
Gson gson) {
this.context = context;
this.okHttpClient = okHttpClient;
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
@ -120,11 +103,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return "Commons/" + ConfigUtils.getVersionNameWithSha(context) + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
}
@VisibleForTesting
public void setWikiMediaToolforgeUrl(String wikiMediaToolforgeUrl) {
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
}
/**
* @param username String
* @param password String
@ -760,44 +738,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return CategoryImageUtils.getMediaList(childNodes);
}
/**
* This method takes search keyword as input and returns a list of Media objects filtered using search query
* It uses the generator query API to get the images searched using a query, 25 at a time.
* @param query keyword to search images on commons
* @return
*/
@Override
@NonNull
public List<Media> searchImages(String query, int offset) {
CustomApiResult apiResult=null;
try {
apiResult= api.action("query")
.param("format", "xml")
.param("generator", "search")
.param("gsrwhat", "text")
.param("gsrnamespace", "6")
.param("gsrlimit", "25")
.param("gsroffset",offset)
.param("gsrsearch", query)
.param("prop", "imageinfo")
.param("iiprop", "url|extmetadata")
.get();
} catch (IOException e) {
Timber.e(e, "Failed to obtain searchImages");
}
CustomApiResult searchImagesNode = apiResult.getNode("/api/query/pages");
if (searchImagesNode == null
|| searchImagesNode.getDocument() == null
|| searchImagesNode.getDocument().getChildNodes() == null
|| searchImagesNode.getDocument().getChildNodes().getLength() == 0) {
return new ArrayList<>();
}
NodeList childNodes = searchImagesNode.getDocument().getChildNodes();
return CategoryImageUtils.getMediaList(childNodes);
}
/**
* This method takes search keyword as input and returns a list of categories objects filtered using search query
* It uses the generator query API to get the categories searched using a query, 25 at a time.
@ -924,25 +864,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
}
}
@Override
@NonNull
public Single<Integer> getUploadCount(String userName) {
final String uploadCountUrlTemplate =
wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/uploadsbyuser.py";
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
uploadCountUrlTemplate,
new PageTitle(userName).getText());
HttpResponse response = Http.get(url).use(httpClient)
.data("user", userName)
.asResponse();
String uploadCount = EntityUtils.toString(response.getEntity()).trim();
return Integer.parseInt(uploadCount);
});
}
/**
* Checks to see if a user is currently blocked from Commons
@ -976,86 +897,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return userBlocked;
}
/**
* This takes userName as input, which is then used to fetch the feedback/achievements
* statistics using OkHttp and JavaRx. This function return JSONObject
* @param userName MediaWiki user name
* @return
*/
@Override
public Single<FeedbackResponse> getAchievements(String userName) {
final String fetchAchievementUrlTemplate =
wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py";
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
fetchAchievementUrlTemplate,
new PageTitle(userName).getText());
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
Timber.d("Response for achievements is %s", json);
try {
return gson.fromJson(json, FeedbackResponse.class);
}
catch (Exception e){
return new FeedbackResponse("",0,0,0,new FeaturedImages(0,0),0,"",0);
}
}
return null;
});
}
/**
* The method returns the picture of the day
*
* @return Media object corresponding to the picture of the day
*/
@Override
@Nullable
public Single<Media> getPictureOfTheDay() {
return Single.fromCallable(() -> {
CustomApiResult apiResult = null;
try {
String template = "Template:Potd/" + DateUtils.getCurrentDate();
CustomMwApi.RequestBuilder requestBuilder = api.action("query")
.param("generator", "images")
.param("format", "xml")
.param("titles", template)
.param("prop", "imageinfo")
.param("iiprop", "url|extmetadata");
apiResult = requestBuilder.get();
} catch (IOException e) {
Timber.e(e, "Failed to obtain searchCategories");
}
if (apiResult == null) {
return null;
}
CustomApiResult imageNode = apiResult.getNode("/api/query/pages/page");
if (imageNode == null
|| imageNode.getDocument() == null) {
return null;
}
return CategoryImageUtils.getMediaFromPage(imageNode.getDocument());
});
}
private Date parseMWDate(String mwDate) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
@ -1076,19 +917,4 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
Timber.e(e, "Error occurred while logging out");
}
}
@Override public Single<CampaignResponseDTO> getCampaigns() {
return Single.fromCallable(() -> {
Request request = new Request.Builder().url(WIKIMEDIA_CAMPAIGNS_BASE_URL).build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
return gson.fromJson(json, CampaignResponseDTO.class);
}
return null;
});
}
}

View file

@ -11,6 +11,8 @@ import java.util.List;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.notification.Notification;
import io.reactivex.Observable;
import io.reactivex.Single;
@ -48,9 +50,6 @@ public interface MediaWikiApi {
List<String> getParentCategoryList(String categoryName);
@NonNull
List<Media> searchImages(String title, int offset);
@NonNull
List<String> searchCategory(String title, int offset);
@ -98,19 +97,11 @@ public interface MediaWikiApi {
@NonNull
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
@NonNull
Single<Integer> getUploadCount(String userName);
boolean isUserBlockedFromCommons();
Single<FeedbackResponse> getAchievements(String userName);
Single<Media> getPictureOfTheDay();
void logout();
Single<CampaignResponseDTO> getCampaigns();
interface ProgressListener {
void onProgress(long transferred, long total);
}

View file

@ -0,0 +1,253 @@
package fr.free.nrw.commons.mwapi;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.gson.Gson;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.achievements.FeaturedImages;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.model.MwQueryPage;
import fr.free.nrw.commons.mwapi.model.MwQueryResponse;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.model.NearbyResponse;
import fr.free.nrw.commons.nearby.model.NearbyResultItem;
import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.utils.DateUtils;
import io.reactivex.Observable;
import io.reactivex.Single;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
@Singleton
public class OkHttpJsonApiClient {
private final OkHttpClient okHttpClient;
private final HttpUrl wikiMediaToolforgeUrl;
private final String sparqlQueryUrl;
private final String campaignsUrl;
private final String commonsBaseUrl;
private Gson gson;
@Inject
public OkHttpJsonApiClient(OkHttpClient okHttpClient,
HttpUrl wikiMediaToolforgeUrl,
String sparqlQueryUrl,
String campaignsUrl,
String commonsBaseUrl,
Gson gson) {
this.okHttpClient = okHttpClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl;
this.commonsBaseUrl = commonsBaseUrl;
this.gson = gson;
}
@NonNull
public Single<Integer> getUploadCount(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
urlBuilder
.addPathSegments("urbanecmbot/commonsmisc/uploadsbyuser.py")
.addQueryParameter("user", userName);
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.isSuccessful()) {
return Integer.parseInt(response.body().string().trim());
}
return 0;
});
}
/**
* This takes userName as input, which is then used to fetch the feedback/achievements
* statistics using OkHttp and JavaRx. This function return JSONObject
*
* @param userName MediaWiki user name
* @return
*/
public Single<FeedbackResponse> getAchievements(String userName) {
final String fetchAchievementUrlTemplate =
wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py";
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
fetchAchievementUrlTemplate,
new PageTitle(userName).getText());
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
Timber.d("Response for achievements is %s", json);
try {
return gson.fromJson(json, FeedbackResponse.class);
} catch (Exception e) {
return new FeedbackResponse("", 0, 0, 0, new FeaturedImages(0, 0), 0, "", 0);
}
}
return null;
});
}
public Observable<List<Place>> getNearbyPlaces(LatLng cur, String lang, double radius) throws IOException {
String wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
String query = wikidataQuery
.replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius))
.replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude()))
.replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude()))
.replace("${LANG}", lang);
HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
.newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("format", "json");
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return new ArrayList<>();
}
NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class);
List<NearbyResultItem> bindings = nearbyResponse.getResults().getBindings();
List<Place> places = new ArrayList<>();
for (NearbyResultItem item : bindings) {
places.add(Place.from(item));
}
return places;
}
return new ArrayList<>();
});
}
public Single<CampaignResponseDTO> getCampaigns() {
return Single.fromCallable(() -> {
Request request = new Request.Builder().url(campaignsUrl)
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
return gson.fromJson(json, CampaignResponseDTO.class);
}
return null;
});
}
/**
* The method returns the picture of the day
*
* @return Media object corresponding to the picture of the day
*/
@Nullable
public Single<Media> getPictureOfTheDay() {
String template = "Template:Potd/" + DateUtils.getCurrentDate();
HttpUrl.Builder urlBuilder = HttpUrl
.parse(commonsBaseUrl)
.newBuilder()
.addQueryParameter("action", "query")
.addQueryParameter("generator", "images")
.addQueryParameter("format", "json")
.addQueryParameter("titles", template)
.addQueryParameter("prop", "imageinfo")
.addQueryParameter("iiprop", "url|extmetadata");
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
MwQueryResponse mwQueryPage = gson.fromJson(json, MwQueryResponse.class);
return Media.from(mwQueryPage.query().firstPage());
}
return null;
});
}
/**
* This method takes search keyword as input and returns a list of Media objects filtered using search query
* It uses the generator query API to get the images searched using a query, 25 at a time.
* @param query keyword to search images on commons
* @return
*/
@Nullable
public Single<List<Media>> searchImages(String query, int offset) {
HttpUrl.Builder urlBuilder = HttpUrl
.parse(commonsBaseUrl)
.newBuilder()
.addQueryParameter("action", "query")
.addQueryParameter("generator", "search")
.addQueryParameter("format", "json")
.addQueryParameter("gsrwhat", "text")
.addQueryParameter("gsrnamespace", "6")
.addQueryParameter("gsrlimit", "25")
.addQueryParameter("gsroffset", String.valueOf(offset))
.addQueryParameter("gsrsearch", query)
.addQueryParameter("prop", "imageinfo")
.addQueryParameter("iiprop", "url|extmetadata");
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
List<Media> mediaList = new ArrayList<>();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return mediaList;
}
MwQueryResponse mwQueryResponse = gson.fromJson(json, MwQueryResponse.class);
List<MwQueryPage> pages = mwQueryResponse.query().pages();
for (MwQueryPage page : pages) {
mediaList.add(Media.from(page));
}
}
return mediaList;
});
}
}

View file

@ -0,0 +1,30 @@
package fr.free.nrw.commons.mwapi.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public class MwException extends RuntimeException {
@SuppressWarnings("unused")
@NonNull
private final MwServiceError error;
public MwException(@NonNull MwServiceError error) {
this.error = error;
}
@NonNull
public MwServiceError getError() {
return error;
}
@Nullable
public String getTitle() {
return error.getTitle();
}
@Override
@Nullable
public String getMessage() {
return error.getDetails();
}
}

View file

@ -0,0 +1,39 @@
package fr.free.nrw.commons.mwapi.model;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
public class MwQueryResponse extends MwResponse {
@SuppressWarnings("unused") @SerializedName("batchcomplete") private boolean batchComplete;
@SuppressWarnings("unused") @SerializedName("continue") @Nullable
private Map<String, String> continuation;
@Nullable private MwQueryResult query;
public boolean batchComplete() {
return batchComplete;
}
@Nullable public Map<String, String> continuation() {
return continuation;
}
@Nullable public MwQueryResult query() {
return query;
}
public boolean success() {
return query != null;
}
@VisibleForTesting
protected void setQuery(@Nullable MwQueryResult query) {
this.query = query;
}
}

View file

@ -0,0 +1,44 @@
package fr.free.nrw.commons.mwapi.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import fr.free.nrw.commons.media.model.ImageInfo;
import fr.free.nrw.commons.media.model.MwQueryPage;
public class MwQueryResult {
@SuppressWarnings("unused")
@Nullable
private HashMap<String, MwQueryPage> pages;
@NonNull
public List<MwQueryPage> pages() {
if (pages == null) {
return new ArrayList<>();
}
return new ArrayList<>(pages.values());
}
@Nullable
public MwQueryPage firstPage() {
return pages().get(0);
}
@NonNull
public Map<String, ImageInfo> images() {
Map<String, ImageInfo> result = new HashMap<>();
if (pages != null) {
for (MwQueryPage page : pages()) {
if (page.imageInfo() != null) {
result.put(page.title(), page.imageInfo());
}
}
}
return result;
}
}

View file

@ -0,0 +1,38 @@
package fr.free.nrw.commons.mwapi.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
import fr.free.nrw.commons.json.PostProcessingTypeAdapter;
public abstract class MwResponse implements PostProcessingTypeAdapter.PostProcessable {
@SuppressWarnings("unused")
@Nullable
private MwServiceError error;
@SuppressWarnings("unused")
@Nullable
private Map<String, Warning> warnings;
@SuppressWarnings("unused,NullableProblems")
@SerializedName("servedby")
@NonNull
private String servedBy;
@Override
public void postProcess() {
if (error != null) {
throw new MwException(error);
}
}
private class Warning {
@SuppressWarnings("unused,NullableProblems")
@NonNull
private String warnings;
}
}

View file

@ -0,0 +1,85 @@
package fr.free.nrw.commons.mwapi.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import fr.free.nrw.commons.utils.StringUtils;
/**
* Gson POJO for a MediaWiki API error.
*/
public class MwServiceError implements ServiceError {
@SuppressWarnings("unused")
@Nullable
private String code;
@SuppressWarnings("unused")
@Nullable
private String info;
@SuppressWarnings("unused")
@Nullable
private String docref;
@SuppressWarnings("unused")
@NonNull
private List<Message> messages = Collections.emptyList();
@Override
@NonNull
public String getTitle() {
return StringUtils.defaultString(code);
}
@Override
@NonNull
public String getDetails() {
return StringUtils.defaultString(info);
}
@Nullable
public String getDocRef() {
return docref;
}
public boolean badToken() {
return "badtoken".equals(code);
}
public boolean badLoginState() {
return "assertuserfailed".equals(code);
}
public boolean hasMessageName(@NonNull String messageName) {
for (Message msg : messages) {
if (messageName.equals(msg.name)) {
return true;
}
}
return false;
}
@Nullable
public String getMessageHtml(@NonNull String messageName) {
for (Message msg : messages) {
if (messageName.equals(msg.name)) {
return msg.html();
}
}
return null;
}
private static final class Message {
@SuppressWarnings("unused")
@Nullable
private String name;
@SuppressWarnings("unused")
@Nullable
private String html;
@NonNull
private String html() {
return StringUtils.defaultString(html);
}
}
}

View file

@ -0,0 +1,13 @@
package fr.free.nrw.commons.mwapi.model;
import android.support.annotation.NonNull;
/**
* The API reported an error in the payload.
*/
public interface ServiceError {
@NonNull
String getTitle();
@NonNull String getDetails();
}

View file

@ -112,7 +112,7 @@ public class NearbyController {
* @param placeList list of nearby places in Place data type
* @return Place list that holds nearby places
*/
public static List<Place> loadAttractionsFromLocationToPlaces(
static List<Place> loadAttractionsFromLocationToPlaces(
LatLng curLatLng,
List<Place> placeList) {
placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS));

View file

@ -1,49 +1,37 @@
package fr.free.nrw.commons.nearby;
import android.net.Uri;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import fr.free.nrw.commons.Utils;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import timber.log.Timber;
/**
* Handles the Wikidata query to obtain Places around search location
*/
@Singleton
public class NearbyPlaces {
private static final double INITIAL_RADIUS = 1.0; // in kilometers
private static final double RADIUS_MULTIPLIER = 1.618;
private static final Uri WIKIDATA_QUERY_URL = Uri.parse("https://query.wikidata.org/sparql");
private static final Uri WIKIDATA_QUERY_UI_URL = Uri.parse("https://query.wikidata.org/");
private final String wikidataQuery;
public double radius = INITIAL_RADIUS;
private final OkHttpJsonApiClient okHttpJsonApiClient;
/**
* Reads Wikidata query to check nearby wikidata items which needs picture, with a circular
* search. As a point is center of a circle with a radius will be set later.
* @param okHttpJsonApiClient
*/
public NearbyPlaces() {
try {
wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
Timber.v(wikidataQuery);
} catch (IOException e) {
throw new RuntimeException(e);
}
@Inject
public NearbyPlaces(OkHttpJsonApiClient okHttpJsonApiClient) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
}
/**
@ -104,81 +92,6 @@ public class NearbyPlaces {
* @throws IOException if query fails
*/
private List<Place> getFromWikidataQuery(LatLng cur, String lang, double radius) throws IOException {
List<Place> places = new ArrayList<>();
String query = wikidataQuery
.replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius))
.replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude()))
.replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude()))
.replace("${LANG}", lang);
Timber.v("# Wikidata query: \n" + query);
// format as a URL
Timber.d(WIKIDATA_QUERY_UI_URL.buildUpon().fragment(query).build().toString());
String url = WIKIDATA_QUERY_URL.buildUpon().appendQueryParameter("query", query).build().toString();
URLConnection conn = new URL(url).openConnection();
conn.setRequestProperty("Accept", "text/tab-separated-values");
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
Timber.d("Reading from query result...");
while ((line = in.readLine()) != null) {
Timber.v(line);
line = line + "\n"; // to pad columns and make fields a fixed size
if (!line.startsWith("\"Point")) {
continue;
}
String[] fields = line.split("\t");
Timber.v("Fields: " + Arrays.toString(fields));
String point = fields[0];
String wikiDataLink = Utils.stripLocalizedString(fields[1]);
String name = Utils.stripLocalizedString(fields[2]);
//getting icon link here
String identifier = Utils.stripLocalizedString(fields[3]);
//getting the ID which is at the end of link
identifier = identifier.split("/")[Utils.stripLocalizedString(fields[3]).split("/").length-1];
//replaced the extra > char from fields
identifier = identifier.replace(">","");
String type = Utils.stripLocalizedString(fields[4]);
String icon = fields[5];
String wikipediaSitelink = Utils.stripLocalizedString(fields[7]);
String commonsSitelink = Utils.stripLocalizedString(fields[8]);
String category = Utils.stripLocalizedString(fields[9]);
Timber.v("Name: " + name + ", type: " + type + ", category: " + category + ", wikipediaSitelink: " + wikipediaSitelink + ", commonsSitelink: " + commonsSitelink);
double latitude;
double longitude;
Matcher matcher = Pattern.compile("Point\\(([^ ]+) ([^ ]+)\\)").matcher(point);
if (!matcher.find()) {
continue;
}
try {
longitude = Double.parseDouble(matcher.group(1));
latitude = Double.parseDouble(matcher.group(2));
} catch (NumberFormatException e) {
throw new RuntimeException("LatLng parse error: " + point);
}
places.add(new Place(
name,
Label.fromText(identifier), // list
type, // details
Uri.parse(icon),
new LatLng(latitude, longitude, 0),
category,
new Sitelinks.Builder()
.setWikipediaLink(wikipediaSitelink)
.setCommonsLink(commonsSitelink)
.setWikidataLink(wikiDataLink)
.build()
));
}
in.close();
return places;
return okHttpJsonApiClient.getNearbyPlaces(cur, lang, radius).blockingSingle();
}
}

View file

@ -7,6 +7,9 @@ import android.os.Parcelable;
import android.support.annotation.Nullable;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.model.NearbyResultItem;
import fr.free.nrw.commons.utils.PlaceUtils;
import fr.free.nrw.commons.utils.StringUtils;
import timber.log.Timber;
/**
@ -48,6 +51,26 @@ public class Place implements Parcelable {
this.siteLinks = in.readParcelable(Sitelinks.class.getClassLoader());
}
public static Place from(NearbyResultItem item) {
String itemClass = item.getClassName().getValue();
String classEntityId = "";
if(!StringUtils.isNullOrWhiteSpace(itemClass)) {
classEntityId = itemClass.replace("http://www.wikidata.org/entity/", "");
}
return new Place(
item.getLabel().getValue(),
Label.fromText(classEntityId), // list
item.getClassLabel().getValue(), // details
Uri.parse(item.getIcon().getValue()),
PlaceUtils.latLngFromPointString(item.getLocation().getValue()),
item.getCommonsCategory().getValue(),
new Sitelinks.Builder()
.setWikipediaLink(item.getWikipediaArticle().getValue())
.setCommonsLink(item.getCommonsArticle().getValue())
.setWikidataLink(item.getItem().getValue())
.build());
}
/**
* Gets the name of the place
* @return name

View file

@ -15,6 +15,7 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.facebook.drawee.view.SimpleDraweeView;
import com.pedrogomez.renderers.Renderer;
import java.util.ArrayList;
@ -43,7 +44,7 @@ public class PlaceRenderer extends Renderer<Place> {
@BindView(R.id.tvName) TextView tvName;
@BindView(R.id.tvDesc) TextView tvDesc;
@BindView(R.id.distance) TextView distance;
@BindView(R.id.icon) ImageView icon;
@BindView(R.id.icon) SimpleDraweeView icon;
@BindView(R.id.buttonLayout) LinearLayout buttonLayout;
@BindView(R.id.cameraButton) LinearLayout cameraButton;
@ -215,7 +216,9 @@ public class PlaceRenderer extends Renderer<Place> {
}
tvDesc.setText(descriptionText);
distance.setText(place.distance);
icon.setImageResource(place.getLabel().getIcon());
icon.setImageURI(place.getSecondaryImageUrl());
directionsButton.setOnClickListener(view -> {
//Open map app at given position

View file

@ -0,0 +1,13 @@
package fr.free.nrw.commons.nearby.model;
public class NearbyResponse {
private final NearbyResults results;
public NearbyResponse(NearbyResults results) {
this.results = results;
}
public NearbyResults getResults() {
return results;
}
}

View file

@ -0,0 +1,70 @@
package fr.free.nrw.commons.nearby.model;
import com.google.gson.annotations.SerializedName;
public class NearbyResultItem {
private final ResultTuple item;
private final ResultTuple wikipediaArticle;
private final ResultTuple commonsArticle;
private final ResultTuple location;
private final ResultTuple label;
private final ResultTuple icon;
@SerializedName("class") private final ResultTuple className;
@SerializedName("class_label") private final ResultTuple classLabel;
@SerializedName("Commons_category") private final ResultTuple commonsCategory;
public NearbyResultItem(ResultTuple item,
ResultTuple wikipediaArticle,
ResultTuple commonsArticle,
ResultTuple location,
ResultTuple label,
ResultTuple icon, ResultTuple className,
ResultTuple classLabel,
ResultTuple commonsCategory) {
this.item = item;
this.wikipediaArticle = wikipediaArticle;
this.commonsArticle = commonsArticle;
this.location = location;
this.label = label;
this.icon = icon;
this.className = className;
this.classLabel = classLabel;
this.commonsCategory = commonsCategory;
}
public ResultTuple getItem() {
return item == null ? new ResultTuple(): item;
}
public ResultTuple getWikipediaArticle() {
return wikipediaArticle == null ? new ResultTuple():wikipediaArticle;
}
public ResultTuple getCommonsArticle() {
return commonsArticle == null ? new ResultTuple():commonsArticle;
}
public ResultTuple getLocation() {
return location == null ? new ResultTuple():location;
}
public ResultTuple getLabel() {
return label == null ? new ResultTuple():label;
}
public ResultTuple getIcon() {
return icon == null ? new ResultTuple():icon;
}
public ResultTuple getClassName() {
return className == null ? new ResultTuple():className;
}
public ResultTuple getClassLabel() {
return classLabel == null ? new ResultTuple():classLabel;
}
public ResultTuple getCommonsCategory() {
return commonsCategory == null ? new ResultTuple():commonsCategory;
}
}

View file

@ -0,0 +1,15 @@
package fr.free.nrw.commons.nearby.model;
import java.util.List;
public class NearbyResults {
private final List<NearbyResultItem> bindings;
public NearbyResults(List<NearbyResultItem> bindings) {
this.bindings = bindings;
}
public List<NearbyResultItem> getBindings() {
return bindings;
}
}

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.nearby.model;
public class ResultTuple {
private final String type;
private final String value;
public ResultTuple(String type, String value) {
this.type = type;
this.value = value;
}
public ResultTuple() {
this.type = "";
this.value = "";
}
public String getType() {
return type;
}
public String getValue() {
return value;
}
}

View file

@ -7,7 +7,7 @@ import android.support.v7.app.AlertDialog.Builder;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
@ -28,7 +28,7 @@ public class QuizChecker {
private CompositeDisposable compositeDisposable = new CompositeDisposable();
public Context context;
private String userName;
private MediaWikiApi mediaWikiApi;
private OkHttpJsonApiClient okHttpJsonApiClient;
private BasicKvStore revertKvStore;
private BasicKvStore countKvStore;
@ -41,16 +41,16 @@ public class QuizChecker {
* constructor to set the parameters for quiz
* @param context context
* @param userName Commons user name
* @param mediaWikiApi instance of MediaWikiApi
* @param okHttpJsonApiClient instance of MediaWikiApi
*/
public QuizChecker(Context context,
String userName,
MediaWikiApi mediaWikiApi,
OkHttpJsonApiClient okHttpJsonApiClient,
BasicKvStore revertKvStore,
BasicKvStore countKvStore) {
this.context = context;
this.userName = userName;
this.mediaWikiApi = mediaWikiApi;
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.revertKvStore = revertKvStore;
this.countKvStore = countKvStore;
setUploadCount();
@ -61,7 +61,7 @@ public class QuizChecker {
* to fet the total number of images uploaded
*/
private void setUploadCount() {
compositeDisposable.add(mediaWikiApi
compositeDisposable.add(okHttpJsonApiClient
.getUploadCount(userName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -89,7 +89,7 @@ public class QuizChecker {
* To call the API to get reverts count in form of JSONObject
*/
private void setRevertCount() {
compositeDisposable.add(mediaWikiApi
compositeDisposable.add(okHttpJsonApiClient
.getAchievements(userName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.utils;
import android.text.format.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
@ -38,11 +39,21 @@ public class DateUtils {
}
}
public static Date getDateFromString(String dateString) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
try {
return dateFormat.parse(dateString);
} catch (ParseException e) {
return null;
}
}
public static String getCurrentDate() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
Date date = new Date();
return dateFormat.format(date);
}
public static String dateInLocaleFormat(Date date){
String formatter;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {

View file

@ -1,5 +1,8 @@
package fr.free.nrw.commons.utils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import fr.free.nrw.commons.location.LatLng;
public class PlaceUtils {
@ -22,4 +25,21 @@ public class PlaceUtils {
String[] parts = latLngString.split("/");
return new LatLng(Double.parseDouble(parts[0]), Double.parseDouble(parts[1]), 0);
}
public static LatLng latLngFromPointString(String pointString) {
double latitude;
double longitude;
Matcher matcher = Pattern.compile("Point\\(([^ ]+) ([^ ]+)\\)").matcher(pointString);
if (!matcher.find()) {
return null;
}
try {
longitude = Double.parseDouble(matcher.group(1));
latitude = Double.parseDouble(matcher.group(2));
} catch (NumberFormatException e) {
return null;
}
return new LatLng(latitude, longitude, 0);
}
}

View file

@ -4,6 +4,8 @@ import android.os.Build;
import android.text.Html;
public class StringUtils {
public static final String EMPTY = "";
public static String getParsedStringFromHtml(String source) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY).toString();
@ -16,4 +18,45 @@ public class StringUtils {
public static boolean isNullOrWhiteSpace(String value) {
return value == null || value.trim().isEmpty();
}
// Defaults
//-----------------------------------------------------------------------
/**
* <p>Returns either the passed in String,
* or if the String is {@code null}, an empty String ("").</p>
*
* <pre>
* StringUtils.defaultString(null) = ""
* StringUtils.defaultString("") = ""
* StringUtils.defaultString("bat") = "bat"
* </pre>
*
* @see String#valueOf(Object)
* @param str the String to check, may be null
* @return the passed in String, or the empty String if it
* was {@code null}
*/
public static String defaultString(final String str) {
return defaultString(str, EMPTY);
}
/**
* <p>Returns either the passed in String, or if the String is
* {@code null}, the value of {@code defaultStr}.</p>
*
* <pre>
* StringUtils.defaultString(null, "NULL") = "NULL"
* StringUtils.defaultString("", "NULL") = ""
* StringUtils.defaultString("bat", "NULL") = "bat"
* </pre>
*
* @see String#valueOf(Object)
* @param str the String to check, may be null
* @param defaultStr the default String to return
* if the input is {@code null}, may be null
* @return the passed in String, or the default if it was {@code null}
*/
public static String defaultString(final String str, final String defaultStr) {
return str == null ? defaultStr : str;
}
}

View file

@ -24,7 +24,7 @@ import javax.inject.Inject;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
@ -37,8 +37,7 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject
MediaWikiApi mediaWikiApi;
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget);
@ -56,7 +55,7 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
RemoteViews views,
AppWidgetManager appWidgetManager,
int appWidgetId) {
compositeDisposable.add(mediaWikiApi.getPictureOfTheDay()
compositeDisposable.add(okHttpJsonApiClient.getPictureOfTheDay()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(

View file

@ -6,7 +6,7 @@
android:focusableInTouchMode="true"
android:minHeight="72dp">
<ImageView
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/icon"
android:layout_width="40dp"
android:layout_height="40dp"
@ -15,7 +15,7 @@
android:layout_marginTop="@dimen/standard_gap"
android:background="@android:color/white"
android:scaleType="centerCrop"
android:src="@drawable/empty_photo"
tools:src="@drawable/empty_photo"
/>
<TextView
@ -27,6 +27,7 @@
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap"
tools:text="900m"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
/>
@ -54,6 +55,7 @@
android:layout_toStartOf="@id/distance"
android:ellipsize="end"
android:maxLines="2"
tools:text="St. Paul's School"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
/>
@ -69,6 +71,7 @@
android:layout_marginBottom="@dimen/standard_gap"
android:ellipsize="end"
android:maxLines="4"
tools:text="school"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
/>

View file

@ -45,7 +45,6 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu
val mockSessionManager: SessionManager = mock()
val locationServiceManager: LocationServiceManager = mock()
val mockDbOpenHelper: DBOpenHelper = mock()
val nearbyPlaces: NearbyPlaces = mock()
val lruCache: LruCache<String, String> = mock()
val gson: Gson = Gson()
val categoryClient: ContentProviderClient = mock()
@ -77,7 +76,5 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu
override fun provideDBOpenHelper(context: Context): DBOpenHelper = mockDbOpenHelper
override fun provideNearbyPlaces(): NearbyPlaces = nearbyPlaces
override fun provideLruCache(): LruCache<String, String> = lruCache
}

View file

@ -41,8 +41,7 @@ class ApacheHttpClientMediaWikiApiTest {
okHttpClient = OkHttpClient()
sharedPreferences = mock(BasicKvStore::class.java)
categoryPreferences = mock(BasicKvStore::class.java)
testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", "http://" + wikidataServer.hostName + ":" + wikidataServer.port + "/", sharedPreferences, categoryPreferences, Gson(), okHttpClient)
testObject.setWikiMediaToolforgeUrl("http://" + server.hostName + ":" + server.port + "/")
testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", "http://" + wikidataServer.hostName + ":" + wikidataServer.port + "/", sharedPreferences, categoryPreferences, Gson())
}
@After
@ -243,17 +242,6 @@ class ApacheHttpClientMediaWikiApiTest {
assertFalse(result)
}
@Test
fun getUploadCount() {
server.enqueue(MockResponse().setBody("23\n"))
val testObserver = testObject.getUploadCount("testUsername").test()
assertEquals("testUsername", parseQueryParams(server.takeRequest())["user"])
assertEquals(1, testObserver.valueCount())
assertEquals(23, testObserver.values()[0])
}
@Test
fun isUserBlockedFromCommonsForInfinitelyBlockedUser() {
server.enqueue(MockResponse().setBody("<?xml version=\"1.0\"?><api><query><userinfo id=\"1000\" name=\"testusername\" blockid=\"3000\" blockedby=\"blockerusername\" blockedbyid=\"1001\" blockreason=\"testing\" blockedtimestamp=\"2018-05-24T15:32:09Z\" blockexpiry=\"infinite\"></userinfo></query></api>"))

View file

@ -25,8 +25,8 @@ Dagger will resolve the method arguments on provider methods in a module (or the
```java
@Provides
@Singleton
public SessionManager providesSessionManager(MediaWikiApi mediaWikiApi) {
return new SessionManager(application, mediaWikiApi);
public SessionManager providesSessionManager(MediaWikiApi okHttpJsonApiClient) {
return new SessionManager(application, okHttpJsonApiClient);
}
```