From f12837650a945c7075813a3e2811f0009321dc0b Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Wed, 6 Feb 2019 10:40:30 +0530 Subject: [PATCH] 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 --- .../fr/free/nrw/commons/BasePresenter.java | 2 + .../main/java/fr/free/nrw/commons/Media.java | 24 ++ .../achievements/AchievementsActivity.java | 7 +- .../pictures/BookmarkPicturesController.java | 19 +- .../pictures/BookmarkPicturesDao.java | 2 + .../commons/campaigns/CampaignsPresenter.java | 23 +- .../nrw/commons/campaigns/ICampaignsView.java | 2 - .../contributions/ContributionsFragment.java | 18 +- .../nrw/commons/delete/ReasonBuilder.java | 59 ++-- .../di/CommonsApplicationComponent.java | 1 + .../commons/di/CommonsApplicationModule.java | 6 - .../free/nrw/commons/di/NetworkingModule.java | 48 ++- .../explore/images/SearchImageFragment.java | 10 +- .../json/PostProcessingTypeAdapter.java | 34 ++ .../commons/media/MediaDetailFragment.java | 40 ++- .../nrw/commons/media/model/ExtMetadata.java | 161 ++++++++++ .../nrw/commons/media/model/ImageInfo.java | 63 ++++ .../nrw/commons/media/model/MwQueryPage.java | 292 ++++++++++++++++++ .../mwapi/ApacheHttpClientMediaWikiApi.java | 176 +---------- .../free/nrw/commons/mwapi/MediaWikiApi.java | 13 +- .../commons/mwapi/OkHttpJsonApiClient.java | 253 +++++++++++++++ .../nrw/commons/mwapi/model/MwException.java | 30 ++ .../commons/mwapi/model/MwQueryResponse.java | 39 +++ .../commons/mwapi/model/MwQueryResult.java | 44 +++ .../nrw/commons/mwapi/model/MwResponse.java | 38 +++ .../commons/mwapi/model/MwServiceError.java | 85 +++++ .../nrw/commons/mwapi/model/ServiceError.java | 13 + .../nrw/commons/nearby/NearbyController.java | 2 +- .../free/nrw/commons/nearby/NearbyPlaces.java | 111 +------ .../fr/free/nrw/commons/nearby/Place.java | 23 ++ .../nrw/commons/nearby/PlaceRenderer.java | 7 +- .../commons/nearby/model/NearbyResponse.java | 13 + .../nearby/model/NearbyResultItem.java | 70 +++++ .../commons/nearby/model/NearbyResults.java | 15 + .../nrw/commons/nearby/model/ResultTuple.java | 24 ++ .../fr/free/nrw/commons/quiz/QuizChecker.java | 14 +- .../fr/free/nrw/commons/utils/DateUtils.java | 11 + .../fr/free/nrw/commons/utils/PlaceUtils.java | 20 ++ .../free/nrw/commons/utils/StringUtils.java | 43 +++ .../nrw/commons/widget/PicOfDayAppWidget.java | 7 +- app/src/main/res/layout/item_place.xml | 7 +- .../nrw/commons/TestCommonsApplication.kt | 3 - .../mwapi/ApacheHttpClientMediaWikiApiTest.kt | 14 +- dependency-injection.md | 4 +- 44 files changed, 1472 insertions(+), 418 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/json/PostProcessingTypeAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java create mode 100644 app/src/main/java/fr/free/nrw/commons/media/model/ImageInfo.java create mode 100644 app/src/main/java/fr/free/nrw/commons/media/model/MwQueryPage.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/model/MwException.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/model/MwQueryResponse.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/model/MwQueryResult.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/model/MwResponse.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/model/MwServiceError.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/model/ServiceError.java create mode 100644 app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResponse.java create mode 100644 app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.java create mode 100644 app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResults.java create mode 100644 app/src/main/java/fr/free/nrw/commons/nearby/model/ResultTuple.java diff --git a/app/src/main/java/fr/free/nrw/commons/BasePresenter.java b/app/src/main/java/fr/free/nrw/commons/BasePresenter.java index 041fde6b2..4124224c3 100644 --- a/app/src/main/java/fr/free/nrw/commons/BasePresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/BasePresenter.java @@ -1,5 +1,7 @@ package fr.free.nrw.commons; +import android.content.Context; + /** * Base presenter, enforcing contracts to atach and detach view */ diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 3ddec01d5..aae4bc935 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -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; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java b/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java index 54b6c15b4..fc5ed60b8 100644 --- a/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java @@ -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()) diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java index 62b5e7d77..c7e74e532 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java @@ -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 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 medias = new ArrayList<>(); for (Bookmark bookmark : bookmarks) { - List tmpMedias = mediaWikiApi.searchImages(bookmark.getMediaName(), 0); + List tmpMedias = okHttpJsonApiClient + .searchImages(bookmark.getMediaName(), 0) + .blockingGet(); for (Media m : tmpMedias) { if (m.getCreator().trim().equals(bookmark.getMediaCreator().trim())) { medias.add(m); diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java index c09b6d8c1..b49f86af7 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java @@ -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 clientProvider; diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java index 59f9d8814..f1ec6321c 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java @@ -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 campaigns = mediaWikiApi.getCampaigns(); + Single campaigns = okHttpJsonApiClient.getCampaigns(); campaigns.observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribeWith(new SingleObserver() { diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java index 8610728b3..699a9c9b4 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java @@ -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); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index ff69d0e2f..083a6deca 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -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 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); diff --git a/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java index 0ba6fe1e3..7ef5d342f 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java @@ -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 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 getReason(Media media, String reason) { + return fetchArticleNumber(media, reason); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index 7e182e717..3b9469cf4 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -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; 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 37d8538e6..491dd1848 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 @@ -191,12 +191,6 @@ public class CommonsApplicationModule { return new DBOpenHelper(context); } - @Provides - @Singleton - public NearbyPlaces provideNearbyPlaces() { - return new NearbyPlaces(); - } - @Provides @Singleton public LruCache provideLruCache() { diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java index 969c60efc..b90c28c71 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java index 2204b0951..3e066d979 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java @@ -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 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) diff --git a/app/src/main/java/fr/free/nrw/commons/json/PostProcessingTypeAdapter.java b/app/src/main/java/fr/free/nrw/commons/json/PostProcessingTypeAdapter.java new file mode 100644 index 000000000..5e0876991 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/json/PostProcessingTypeAdapter.java @@ -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 TypeAdapter create(Gson gson, TypeToken type) { + final TypeAdapter delegate = gson.getDelegateAdapter(this, type); + + return new TypeAdapter() { + 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; + } + }; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index e2dccc5df..4ec6d03a5 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -1,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 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) { diff --git a/app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java b/app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java new file mode 100644 index 000000000..60862718b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/model/ExtMetadata.java @@ -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; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/media/model/ImageInfo.java b/app/src/main/java/fr/free/nrw/commons/media/model/ImageInfo.java new file mode 100644 index 000000000..9e5f00771 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/model/ImageInfo.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/media/model/MwQueryPage.java b/app/src/main/java/fr/free/nrw/commons/media/model/MwQueryPage.java new file mode 100644 index 000000000..bda91c499 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/model/MwQueryPage.java @@ -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 langlinks; + @SuppressWarnings("unused") + @Nullable + private List revisions; + @SuppressWarnings("unused") + @Nullable + private List coordinates; + @SuppressWarnings("unused") + @Nullable + private List 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; + @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 langLinks() { + return langlinks; + } + + @Nullable + public List revisions() { + return revisions; + } + + @Nullable + public List categories() { + return categories; + } + + @Nullable + public List 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; + } + } +} \ No newline at end of file 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 a4c355d0d..15efba8ed 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 @@ -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 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 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 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 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 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; - }); - } } 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 1d4566c40..3cc3f4e5d 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 @@ -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 getParentCategoryList(String categoryName); - @NonNull - List searchImages(String title, int offset); - @NonNull List 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 getUploadCount(String userName); boolean isUserBlockedFromCommons(); - Single getAchievements(String userName); - - Single getPictureOfTheDay(); - void logout(); - Single getCampaigns(); - interface ProgressListener { void onProgress(long transferred, long total); } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java new file mode 100644 index 000000000..c755c760e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -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 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 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> 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 bindings = nearbyResponse.getResults().getBindings(); + List places = new ArrayList<>(); + for (NearbyResultItem item : bindings) { + places.add(Place.from(item)); + } + return places; + } + return new ArrayList<>(); + }); + } + + public Single 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 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> 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 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 pages = mwQueryResponse.query().pages(); + for (MwQueryPage page : pages) { + mediaList.add(Media.from(page)); + } + } + return mediaList; + }); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwException.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwException.java new file mode 100644 index 000000000..baeebb3cb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwException.java @@ -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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwQueryResponse.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwQueryResponse.java new file mode 100644 index 000000000..c305a0473 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwQueryResponse.java @@ -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 continuation; + + @Nullable private MwQueryResult query; + + public boolean batchComplete() { + return batchComplete; + } + + @Nullable public Map 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwQueryResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwQueryResult.java new file mode 100644 index 000000000..a35656e4c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwQueryResult.java @@ -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 pages; + + @NonNull + public List pages() { + if (pages == null) { + return new ArrayList<>(); + } + return new ArrayList<>(pages.values()); + } + + @Nullable + public MwQueryPage firstPage() { + return pages().get(0); + } + + @NonNull + public Map images() { + Map result = new HashMap<>(); + if (pages != null) { + for (MwQueryPage page : pages()) { + if (page.imageInfo() != null) { + result.put(page.title(), page.imageInfo()); + } + } + } + return result; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwResponse.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwResponse.java new file mode 100644 index 000000000..3f15dc459 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwResponse.java @@ -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 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwServiceError.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwServiceError.java new file mode 100644 index 000000000..88c556578 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/MwServiceError.java @@ -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 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); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/ServiceError.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/ServiceError.java new file mode 100644 index 000000000..4fc73b601 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/ServiceError.java @@ -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(); +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 94c129ede..212efd4f1 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -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 loadAttractionsFromLocationToPlaces( + static List loadAttractionsFromLocationToPlaces( LatLng curLatLng, List placeList) { placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS)); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index f4c733708..35ace140c 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -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 getFromWikidataQuery(LatLng cur, String lang, double radius) throws IOException { - List 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(); } } 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 cccfa88c1..9da368a4e 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 @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index 447a5aef2..0e5ee75c9 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java @@ -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 { @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 { } 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 diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResponse.java b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResponse.java new file mode 100644 index 000000000..089bec8a9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResponse.java @@ -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; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.java b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.java new file mode 100644 index 000000000..88e498c1b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.java @@ -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; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResults.java b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResults.java new file mode 100644 index 000000000..046ffbc72 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResults.java @@ -0,0 +1,15 @@ +package fr.free.nrw.commons.nearby.model; + +import java.util.List; + +public class NearbyResults { + private final List bindings; + + public NearbyResults(List bindings) { + this.bindings = bindings; + } + + public List getBindings() { + return bindings; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/ResultTuple.java b/app/src/main/java/fr/free/nrw/commons/nearby/model/ResultTuple.java new file mode 100644 index 000000000..a1916706f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/ResultTuple.java @@ -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; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java index 65006e06a..d3941e255 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java @@ -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()) diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java index 62e0fc19a..8da4c91ec 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java @@ -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) { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java index 4a4e153db..c32dc7b0a 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java @@ -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); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java index 0f93e65ef..62191ec9f 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java @@ -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 + //----------------------------------------------------------------------- + /** + *

Returns either the passed in String, + * or if the String is {@code null}, an empty String ("").

+ * + *
+     * StringUtils.defaultString(null)  = ""
+     * StringUtils.defaultString("")    = ""
+     * StringUtils.defaultString("bat") = "bat"
+     * 
+ * + * @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); + } + + /** + *

Returns either the passed in String, or if the String is + * {@code null}, the value of {@code defaultStr}.

+ * + *
+     * StringUtils.defaultString(null, "NULL")  = "NULL"
+     * StringUtils.defaultString("", "NULL")    = ""
+     * StringUtils.defaultString("bat", "NULL") = "bat"
+     * 
+ * + * @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; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java index fc87f7abb..77f992c8a 100644 --- a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java +++ b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java @@ -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( diff --git a/app/src/main/res/layout/item_place.xml b/app/src/main/res/layout/item_place.xml index eb64e8de0..fd09377ec 100644 --- a/app/src/main/res/layout/item_place.xml +++ b/app/src/main/res/layout/item_place.xml @@ -6,7 +6,7 @@ android:focusableInTouchMode="true" android:minHeight="72dp"> - @@ -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" /> diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt index 5f2340dfe..6e5c33a46 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt @@ -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 = 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 = lruCache } \ No newline at end of file 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 8f320109d..eb85e4b71 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 @@ -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("")) diff --git a/dependency-injection.md b/dependency-injection.md index f29d7b603..c4a4d6fc4 100644 --- a/dependency-injection.md +++ b/dependency-injection.md @@ -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); } ```