consumer, ProducerContext context) {
+ return new OkHttpNetworkFetchState(consumer, context);
+ }
- fetchState
- .getContext()
- .addCallbacks(
- new BaseProducerContextCallbacks() {
- @Override
- public void onCancellationRequested() {
- if (Looper.myLooper() != Looper.getMainLooper()) {
- call.cancel();
- } else {
- mCancellationExecutor.execute(
- new Runnable() {
- @Override
- public void run() {
- call.cancel();
- }
- });
- }
- }
- });
+ @Override
+ public void fetch(
+ final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) {
+ fetchState.submitTime = SystemClock.elapsedRealtime();
+ final Uri uri = fetchState.getUri();
- call.enqueue(
- new okhttp3.Callback() {
- @Override
- public void onResponse(Call call, Response response) throws IOException {
- fetchState.responseTime = SystemClock.elapsedRealtime();
- final ResponseBody body = response.body();
- try {
- if (!response.isSuccessful()) {
- handleException(
- call, new IOException("Unexpected HTTP code " + response), callback);
+ try {
+ if (defaultKvStore
+ .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
+ Timber.d("Skipping loading of image as limited connection mode is enabled");
+ callback.onFailure(
+ new Exception("Failing image request as limited connection mode is enabled"));
return;
- }
-
- BytesRange responseRange =
- BytesRange.fromContentRangeHeader(response.header("Content-Range"));
- if (responseRange != null
- && !(responseRange.from == 0
- && responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
- // Only treat as a partial image if the range is not all of the content
- fetchState.setResponseBytesRange(responseRange);
- fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
- }
-
- long contentLength = body.contentLength();
- if (contentLength < 0) {
- contentLength = 0;
- }
- callback.onResponse(body.byteStream(), (int) contentLength);
- } catch (Exception e) {
- handleException(call, e, callback);
- } finally {
- body.close();
}
- }
+ final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get();
- @Override
- public void onFailure(Call call, IOException e) {
- handleException(call, e, callback);
- }
- });
- }
+ if (mCacheControl != null) {
+ requestBuilder.cacheControl(mCacheControl);
+ }
- /**
- * Handles exceptions.
- *
- * OkHttp notifies callers of cancellations via an IOException. If IOException is caught after
- * request cancellation, then the exception is interpreted as successful cancellation and
- * onCancellation is called. Otherwise onFailure is called.
- */
- private void handleException(final Call call, final Exception e, final Callback callback) {
- if (call.isCanceled()) {
- callback.onCancellation();
- } else {
- callback.onFailure(e);
+ final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange();
+ if (bytesRange != null) {
+ requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue());
+ }
+
+ fetchWithRequest(fetchState, callback, requestBuilder.build());
+ } catch (Exception e) {
+ // handle error while creating the request
+ callback.onFailure(e);
+ }
}
- }
- public static class OkHttpNetworkFetchState extends FetchState {
-
- public long submitTime;
- public long responseTime;
- public long fetchCompleteTime;
-
- public OkHttpNetworkFetchState(
- Consumer consumer, ProducerContext producerContext) {
- super(consumer, producerContext);
+ @Override
+ public void onFetchCompletion(OkHttpNetworkFetchState fetchState, int byteSize) {
+ fetchState.fetchCompleteTime = SystemClock.elapsedRealtime();
+ }
+
+ @Override
+ public Map getExtraMap(OkHttpNetworkFetchState fetchState, int byteSize) {
+ Map extraMap = new HashMap<>(4);
+ extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
+ extraMap
+ .put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
+ extraMap
+ .put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
+ extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
+ return extraMap;
+ }
+
+ protected void fetchWithRequest(
+ final OkHttpNetworkFetchState fetchState,
+ final NetworkFetcher.Callback callback,
+ final Request request) {
+ final Call call = mCallFactory.newCall(request);
+
+ fetchState
+ .getContext()
+ .addCallbacks(
+ new BaseProducerContextCallbacks() {
+ @Override
+ public void onCancellationRequested() {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ call.cancel();
+ } else {
+ mCancellationExecutor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ call.cancel();
+ }
+ });
+ }
+ }
+ });
+
+ call.enqueue(
+ new okhttp3.Callback() {
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ fetchState.responseTime = SystemClock.elapsedRealtime();
+ final ResponseBody body = response.body();
+ try {
+ if (!response.isSuccessful()) {
+ handleException(
+ call, new IOException("Unexpected HTTP code " + response),
+ callback);
+ return;
+ }
+
+ BytesRange responseRange =
+ BytesRange.fromContentRangeHeader(response.header("Content-Range"));
+ if (responseRange != null
+ && !(responseRange.from == 0
+ && responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
+ // Only treat as a partial image if the range is not all of the content
+ fetchState.setResponseBytesRange(responseRange);
+ fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
+ }
+
+ long contentLength = body.contentLength();
+ if (contentLength < 0) {
+ contentLength = 0;
+ }
+ callback.onResponse(body.byteStream(), (int) contentLength);
+ } catch (Exception e) {
+ handleException(call, e, callback);
+ } finally {
+ body.close();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, IOException e) {
+ handleException(call, e, callback);
+ }
+ });
+ }
+
+ /**
+ * Handles exceptions.
+ *
+ * OkHttp notifies callers of cancellations via an IOException. If IOException is caught
+ * after
+ * request cancellation, then the exception is interpreted as successful cancellation and
+ * onCancellation is called. Otherwise onFailure is called.
+ */
+ private void handleException(final Call call, final Exception e, final Callback callback) {
+ if (call.isCanceled()) {
+ callback.onCancellation();
+ } else {
+ callback.onFailure(e);
+ }
+ }
+
+ public static class OkHttpNetworkFetchState extends FetchState {
+
+ public long submitTime;
+ public long responseTime;
+ public long fetchCompleteTime;
+
+ public OkHttpNetworkFetchState(
+ Consumer consumer, ProducerContext producerContext) {
+ super(consumer, producerContext);
+ }
}
- }
}
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 8e6518c44..d913ff149 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
@@ -18,16 +18,20 @@ import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.OnKeyListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.webkit.WebView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
+import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.SearchView;
@@ -86,6 +90,7 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
+import org.wikipedia.language.AppLanguageLookUpTable;
import org.wikipedia.util.DateUtil;
import timber.log.Timber;
@@ -140,7 +145,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
JsonKvStore applicationKvStore;
private int initialListTop = 0;
-
+ @BindView(R.id.description_webview)
+ WebView descriptionWebView;
@BindView(R.id.mediaDetailFrameLayout)
FrameLayout frameLayout;
@BindView(R.id.mediaDetailImageView)
@@ -203,6 +209,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
TextView existingCategories;
@BindView(R.id.no_results_found)
TextView noResultsFound;
+ @BindView(R.id.dummy_caption_description_container)
+ LinearLayout showCaptionAndDescriptionContainer;
+ @BindView(R.id.show_caption_description_textview)
+ TextView showCaptionDescriptionTextView;
+ @BindView(R.id.caption_listview)
+ ListView captionsListView;
+ @BindView(R.id.caption_label)
+ TextView captionLabel;
+ @BindView(R.id.description_label)
+ TextView descriptionLabel;
+ @BindView(R.id.pb_circular)
+ ProgressBar progressBar;
+ String descriptionHtmlCode;
@BindView(R.id.progressBarDeletion)
ProgressBar progressBarDeletion;
@@ -302,6 +321,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
if(applicationKvStore.getBoolean("login_skipped")){
delete.setVisibility(GONE);
}
+
+ handleBackEvent(view);
+
/**
* Gets the height of the frame layout as soon as the view is ready and updates aspect ratio
* of the picture.
@@ -317,14 +339,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
return view;
}
- @Override
- public void onAttach(final Context context) {
- super.onAttach(context);
- if (getParentFragment() != null) {
- callback = (Callback) getParentFragment();
- }
- }
-
@OnClick(R.id.mediaDetailImageViewSpacer)
public void launchZoomActivity(View view) {
if (media.getImageUrl() != null) {
@@ -626,6 +640,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
categoryEditSearchRecyclerViewAdapter.addToCategories(media.getCategories());
updateSelectedCategoriesTextView(categoryEditSearchRecyclerViewAdapter.getCategories());
+ categoryRecyclerView.setVisibility(GONE);
updateCategoryList();
if (media.getAuthor() == null || media.getAuthor().equals("")) {
@@ -661,20 +676,28 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
public void updateSelectedCategoriesTextView(List selectedCategories) {
if (selectedCategories == null || selectedCategories.size() == 0) {
updateCategoriesButton.setClickable(false);
- }
- if (selectedCategories != null) {
+ updateCategoriesButton.setAlpha(.5f);
+ } else {
existingCategories.setText(StringUtils.join(selectedCategories,", "));
- updateCategoriesButton.setClickable(true);
+ if (selectedCategories.equals(media.getCategories())) {
+ updateCategoriesButton.setClickable(false);
+ updateCategoriesButton.setAlpha(.5f);
+ } else {
+ updateCategoriesButton.setClickable(true);
+ updateCategoriesButton.setAlpha(1f);
+ }
}
}
@Override
public void noResultsFound() {
+ categoryRecyclerView.setVisibility(GONE);
noResultsFound.setVisibility(VISIBLE);
}
@Override
public void someResultsFound() {
+ categoryRecyclerView.setVisibility(VISIBLE);
noResultsFound.setVisibility(GONE);
}
@@ -760,6 +783,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
}
public void displayHideCategorySearch() {
+ showCaptionAndDescriptionContainer.setVisibility(GONE);
if (dummyCategoryEditContainer.getVisibility() != VISIBLE) {
dummyCategoryEditContainer.setVisibility(VISIBLE);
} else {
@@ -791,6 +815,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
.defaultLocation(new CameraPosition.Builder()
.target(new LatLng(defaultLatitude, defaultLongitude))
.zoom(16).build())
+ .activityKey("MediaActivity")
.build(getActivity()), REQUEST_CODE);
}
@@ -1127,6 +1152,97 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
}
}
+ @OnClick(R.id.show_caption_description_textview)
+ void showCaptionAndDescription() {
+ dummyCategoryEditContainer.setVisibility(GONE);
+ if (showCaptionAndDescriptionContainer.getVisibility() == GONE) {
+ showCaptionAndDescriptionContainer.setVisibility(VISIBLE);
+ setUpCaptionAndDescriptionLayout();
+ } else {
+ showCaptionAndDescriptionContainer.setVisibility(GONE);
+ }
+ }
+
+ /**
+ * setUp Caption And Description Layout
+ */
+ private void setUpCaptionAndDescriptionLayout() {
+ List captions = getCaptions();
+
+ if (descriptionHtmlCode == null) {
+ progressBar.setVisibility(VISIBLE);
+ }
+
+ getDescription();
+ CaptionListViewAdapter adapter = new CaptionListViewAdapter(captions);
+ captionsListView.setAdapter(adapter);
+ }
+
+ /**
+ * Generate the caption with language
+ */
+ private List getCaptions() {
+ List captionList = new ArrayList<>();
+ Map captions = media.getCaptions();
+ AppLanguageLookUpTable appLanguageLookUpTable = new AppLanguageLookUpTable(getContext());
+ for (Map.Entry map : captions.entrySet()) {
+ String language = appLanguageLookUpTable.getLocalizedName(map.getKey());
+ String languageCaption = map.getValue();
+ captionList.add(new Caption(language, languageCaption));
+ }
+
+ if (captionList.size() == 0) {
+ captionList.add(new Caption("", "No Caption"));
+ }
+ return captionList;
+ }
+
+ private void getDescription() {
+ compositeDisposable.add(mediaDataExtractor.getHtmlOfPage(media.getFilename())
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(this::extractDescription, Timber::e));
+ }
+
+ /**
+ * extract the description from html of imagepage
+ */
+ private void extractDescription(String s) {
+ String descriptionClassName = "| ";
+ int start = s.indexOf(descriptionClassName) + descriptionClassName.length();
+ int end = s.indexOf(" | ", start);
+ descriptionHtmlCode = "";
+ for (int i = start; i < end; i++) {
+ descriptionHtmlCode = descriptionHtmlCode + s.toCharArray()[i];
+ }
+
+ descriptionWebView
+ .loadDataWithBaseURL(null, descriptionHtmlCode, "text/html", "utf-8", null);
+ progressBar.setVisibility(GONE);
+ }
+
+ /**
+ * Handle back event when fragment when showCaptionAndDescriptionContainer is visible
+ */
+ private void handleBackEvent(View view) {
+ view.setFocusableInTouchMode(true);
+ view.requestFocus();
+ view.setOnKeyListener(new OnKeyListener() {
+ @Override
+ public boolean onKey(View view, int keycode, KeyEvent keyEvent) {
+ if (keycode == KeyEvent.KEYCODE_BACK) {
+ if (showCaptionAndDescriptionContainer.getVisibility() == VISIBLE) {
+ showCaptionAndDescriptionContainer.setVisibility(GONE);
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+
+ }
+
+
public interface Callback {
void nominatingForDeletion(int index);
}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java
index ef9559c29..86be5c875 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java
@@ -3,6 +3,7 @@ package fr.free.nrw.commons.media;
import io.reactivex.Observable;
import io.reactivex.Single;
import org.wikipedia.wikidata.Entities;
+import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
index 328f50e3b..f76e39825 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
@@ -12,6 +12,8 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@@ -100,7 +102,16 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
pager.addOnPageChangeListener(this);
adapter = new MediaDetailAdapter(getChildFragmentManager());
+ ((BaseActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ if (getActivity() != null) {
+ final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
pager.setAdapter(adapter);
+
if (savedInstanceState != null) {
final int pageNumber = savedInstanceState.getInt("current-page");
pager.setCurrentItem(pageNumber, false);
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java
index 2ff346085..c354ba78b 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java
@@ -3,6 +3,7 @@ package fr.free.nrw.commons.media;
import io.reactivex.Single;
import java.util.Map;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
+import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;
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
index 6b306690b..4b7146487 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java
@@ -42,319 +42,325 @@ import timber.log.Timber;
@Singleton
public class OkHttpJsonApiClient {
- private final OkHttpClient okHttpClient;
- private final DepictsClient depictsClient;
- private final HttpUrl wikiMediaToolforgeUrl;
- private final HttpUrl wikiMediaTestToolforgeUrl;
- private final String sparqlQueryUrl;
- private final String campaignsUrl;
- private final Gson gson;
+ private final OkHttpClient okHttpClient;
+ private final DepictsClient depictsClient;
+ private final HttpUrl wikiMediaToolforgeUrl;
+ private final HttpUrl wikiMediaTestToolforgeUrl;
+ private final String sparqlQueryUrl;
+ private final String campaignsUrl;
+ private final Gson gson;
- @Inject
- public OkHttpJsonApiClient(OkHttpClient okHttpClient,
- DepictsClient depictsClient,
- HttpUrl wikiMediaToolforgeUrl,
- HttpUrl wikiMediaTestToolforgeUrl,
- String sparqlQueryUrl,
- String campaignsUrl,
- Gson gson) {
- this.okHttpClient = okHttpClient;
- this.depictsClient = depictsClient;
- this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
- this.wikiMediaTestToolforgeUrl = wikiMediaTestToolforgeUrl;
- this.sparqlQueryUrl = sparqlQueryUrl;
- this.campaignsUrl = campaignsUrl;
- this.gson = gson;
- }
-
- /**
- * The method will gradually calls the leaderboard API and fetches the leaderboard
- * @param userName username of leaderboard user
- * @param duration duration for leaderboard
- * @param category category for leaderboard
- * @param limit page size limit for list
- * @param offset offset for the list
- * @return LeaderboardResponse object
- */
- @NonNull
- public Observable getLeaderboard(String userName, String duration, String category, String limit, String offset) {
- final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
- + LEADERBOARD_END_POINT;
- String url = String.format(Locale.ENGLISH,
- fetchLeaderboardUrlTemplate,
- userName,
- duration,
- category,
- limit,
- offset);
- HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
- urlBuilder.addQueryParameter("user", userName);
- urlBuilder.addQueryParameter("duration", duration);
- urlBuilder.addQueryParameter("category", category);
- urlBuilder.addQueryParameter("limit", limit);
- urlBuilder.addQueryParameter("offset", offset);
- Timber.i("Url %s", urlBuilder.toString());
- Request request = new Request.Builder()
- .url(urlBuilder.toString())
- .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 LeaderboardResponse();
- }
- Timber.d("Response for leaderboard is %s", json);
- try {
- return gson.fromJson(json, LeaderboardResponse.class);
- } catch (Exception e) {
- return new LeaderboardResponse();
- }
- }
- return new LeaderboardResponse();
- });
- }
-
- /**
- * This method will update the leaderboard user avatar
- * @param username username to update
- * @param avatar url of the new avatar
- * @return UpdateAvatarResponse object
- */
- @NonNull
- public Single setAvatar(String username, String avatar) {
- final String urlTemplate = wikiMediaTestToolforgeUrl
- + UPDATE_AVATAR_END_POINT;
- return Single.fromCallable(() -> {
- String url = String.format(Locale.ENGLISH,
- urlTemplate,
- username,
- avatar);
- HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
- urlBuilder.addQueryParameter("user", username);
- urlBuilder.addQueryParameter("avatar", avatar);
- 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;
- }
- try {
- return gson.fromJson(json, UpdateAvatarResponse.class);
- } catch (Exception e) {
- return new UpdateAvatarResponse();
- }
- }
- return null;
- });
- }
-
- @NonNull
- public Single getUploadCount(String userName) {
- HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
- urlBuilder
- .addPathSegments("uploadsbyuser.py")
- .addQueryParameter("user", userName);
-
- if (ConfigUtils.isBetaFlavour()) {
- urlBuilder.addQueryParameter("labs", "commonswiki");
+ @Inject
+ public OkHttpJsonApiClient(OkHttpClient okHttpClient,
+ DepictsClient depictsClient,
+ HttpUrl wikiMediaToolforgeUrl,
+ HttpUrl wikiMediaTestToolforgeUrl,
+ String sparqlQueryUrl,
+ String campaignsUrl,
+ Gson gson) {
+ this.okHttpClient = okHttpClient;
+ this.depictsClient = depictsClient;
+ this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
+ this.wikiMediaTestToolforgeUrl = wikiMediaTestToolforgeUrl;
+ this.sparqlQueryUrl = sparqlQueryUrl;
+ this.campaignsUrl = campaignsUrl;
+ this.gson = gson;
}
- Request request = new Request.Builder()
- .url(urlBuilder.build())
- .build();
-
- return Single.fromCallable(() -> {
- Response response = okHttpClient.newCall(request).execute();
- if (response != null && response.isSuccessful()) {
- ResponseBody responseBody = response.body();
- if (null != responseBody) {
- String responseBodyString = responseBody.string().trim();
- if (!TextUtils.isEmpty(responseBodyString)) {
- try {
- return Integer.parseInt(responseBodyString);
- } catch (NumberFormatException e) {
- Timber.e(e);
+ /**
+ * The method will gradually calls the leaderboard API and fetches the leaderboard
+ *
+ * @param userName username of leaderboard user
+ * @param duration duration for leaderboard
+ * @param category category for leaderboard
+ * @param limit page size limit for list
+ * @param offset offset for the list
+ * @return LeaderboardResponse object
+ */
+ @NonNull
+ public Observable getLeaderboard(String userName, String duration,
+ String category, String limit, String offset) {
+ final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
+ + LEADERBOARD_END_POINT;
+ String url = String.format(Locale.ENGLISH,
+ fetchLeaderboardUrlTemplate,
+ userName,
+ duration,
+ category,
+ limit,
+ offset);
+ HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
+ urlBuilder.addQueryParameter("user", userName);
+ urlBuilder.addQueryParameter("duration", duration);
+ urlBuilder.addQueryParameter("category", category);
+ urlBuilder.addQueryParameter("limit", limit);
+ urlBuilder.addQueryParameter("offset", offset);
+ Timber.i("Url %s", urlBuilder.toString());
+ Request request = new Request.Builder()
+ .url(urlBuilder.toString())
+ .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 LeaderboardResponse();
+ }
+ Timber.d("Response for leaderboard is %s", json);
+ try {
+ return gson.fromJson(json, LeaderboardResponse.class);
+ } catch (Exception e) {
+ return new LeaderboardResponse();
+ }
}
- }
- }
- }
- return 0;
- });
- }
-
- @NonNull
- public Single getWikidataEdits(String userName) {
- HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
- urlBuilder
- .addPathSegments("wikidataedits.py")
- .addQueryParameter("user", userName);
-
- if (ConfigUtils.isBetaFlavour()) {
- urlBuilder.addQueryParameter("labs", "commonswiki");
+ return new LeaderboardResponse();
+ });
}
- Request request = new Request.Builder()
- .url(urlBuilder.build())
- .build();
+ /**
+ * This method will update the leaderboard user avatar
+ *
+ * @param username username to update
+ * @param avatar url of the new avatar
+ * @return UpdateAvatarResponse object
+ */
+ @NonNull
+ public Single setAvatar(String username, String avatar) {
+ final String urlTemplate = wikiMediaTestToolforgeUrl
+ + UPDATE_AVATAR_END_POINT;
+ return Single.fromCallable(() -> {
+ String url = String.format(Locale.ENGLISH,
+ urlTemplate,
+ username,
+ avatar);
+ HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
+ urlBuilder.addQueryParameter("user", username);
+ urlBuilder.addQueryParameter("avatar", avatar);
+ 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;
+ }
+ try {
+ return gson.fromJson(json, UpdateAvatarResponse.class);
+ } catch (Exception e) {
+ return new UpdateAvatarResponse();
+ }
+ }
+ return null;
+ });
+ }
- return Single.fromCallable(() -> {
- Response response = okHttpClient.newCall(request).execute();
- if (response != null &&
- response.isSuccessful() && response.body() != null) {
- String json = response.body().string();
- if (json == null) {
- return 0;
- }
- GetWikidataEditCountResponse countResponse = gson
- .fromJson(json, GetWikidataEditCountResponse.class);
- if (null != countResponse) {
- return countResponse.getWikidataEditCount();
- }
- }
- return 0;
- });
- }
+ @NonNull
+ public Single getUploadCount(String userName) {
+ HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
+ urlBuilder
+ .addPathSegments("uploadsbyuser.py")
+ .addQueryParameter("user", userName);
- /**
- * 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 + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki"
- : "/feedback.py");
- return Single.fromCallable(() -> {
- String url = String.format(
- Locale.ENGLISH,
- fetchAchievementUrlTemplate,
- userName);
- HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
- urlBuilder.addQueryParameter("user", userName);
- 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, "");
+ if (ConfigUtils.isBetaFlavour()) {
+ urlBuilder.addQueryParameter("labs", "commonswiki");
}
+ Request request = new Request.Builder()
+ .url(urlBuilder.build())
+ .build();
- }
- return null;
- });
- }
+ return Single.fromCallable(() -> {
+ Response response = okHttpClient.newCall(request).execute();
+ if (response != null && response.isSuccessful()) {
+ ResponseBody responseBody = response.body();
+ if (null != responseBody) {
+ String responseBodyString = responseBody.string().trim();
+ if (!TextUtils.isEmpty(responseBodyString)) {
+ try {
+ return Integer.parseInt(responseBodyString);
+ } catch (NumberFormatException e) {
+ Timber.e(e);
+ }
+ }
+ }
+ }
+ return 0;
+ });
+ }
- public Observable> getNearbyPlaces(LatLng cur, String language, double radius) throws IOException {
+ @NonNull
+ public Single getWikidataEdits(String userName) {
+ HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
+ urlBuilder
+ .addPathSegments("wikidataedits.py")
+ .addQueryParameter("user", userName);
+
+ if (ConfigUtils.isBetaFlavour()) {
+ urlBuilder.addQueryParameter("labs", "commonswiki");
+ }
+
+ Request request = new Request.Builder()
+ .url(urlBuilder.build())
+ .build();
+
+ return Single.fromCallable(() -> {
+ Response response = okHttpClient.newCall(request).execute();
+ if (response != null &&
+ response.isSuccessful() && response.body() != null) {
+ String json = response.body().string();
+ if (json == null) {
+ return 0;
+ }
+ GetWikidataEditCountResponse countResponse = gson
+ .fromJson(json, GetWikidataEditCountResponse.class);
+ if (null != countResponse) {
+ return countResponse.getWikidataEditCount();
+ }
+ }
+ 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 + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki"
+ : "/feedback.py");
+ return Single.fromCallable(() -> {
+ String url = String.format(
+ Locale.ENGLISH,
+ fetchAchievementUrlTemplate,
+ userName);
+ HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
+ urlBuilder.addQueryParameter("user", userName);
+ 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, "");
+ }
+
+
+ }
+ return null;
+ });
+ }
+
+ public Observable> getNearbyPlaces(LatLng cur, String language, 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}", language);
+ .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}", language);
- HttpUrl.Builder urlBuilder = HttpUrl
- .parse(sparqlQueryUrl)
- .newBuilder()
- .addQueryParameter("query", query)
- .addQueryParameter("format", "json");
+ HttpUrl.Builder urlBuilder = HttpUrl
+ .parse(sparqlQueryUrl)
+ .newBuilder()
+ .addQueryParameter("query", query)
+ .addQueryParameter("format", "json");
- Request request = new Request.Builder()
- .url(urlBuilder.build())
- .build();
+ 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<>();
- });
- }
+ 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<>();
+ });
+ }
- /**
- * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
- * bridge -> suspended bridge, aqueduct, etc
- */
- public Single> getChildDepictions(String qid, int startPosition,
- int limit) throws IOException {
- return depictedItemsFrom(sparqlQuery(qid, startPosition, limit,"/queries/subclasses_query.rq"));
- }
+ /**
+ * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
+ * bridge -> suspended bridge, aqueduct, etc
+ */
+ public Single> getChildDepictions(String qid, int startPosition,
+ int limit) throws IOException {
+ return depictedItemsFrom(
+ sparqlQuery(qid, startPosition, limit, "/queries/subclasses_query.rq"));
+ }
- /**
- * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
- * bridge -> suspended bridge, aqueduct, etc
- */
- public Single> getParentDepictions(String qid, int startPosition,
- int limit) throws IOException {
- return depictedItemsFrom(sparqlQuery(qid, startPosition, limit,
- "/queries/parentclasses_query.rq"));
- }
+ /**
+ * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
+ * bridge -> suspended bridge, aqueduct, etc
+ */
+ public Single> getParentDepictions(String qid, int startPosition,
+ int limit) throws IOException {
+ return depictedItemsFrom(sparqlQuery(qid, startPosition, limit,
+ "/queries/parentclasses_query.rq"));
+ }
- private Single> depictedItemsFrom(Request request) {
- return depictsClient.toDepictions(Single.fromCallable(() -> {
- try (ResponseBody body = okHttpClient.newCall(request).execute().body()) {
- return gson.fromJson(body.string(), SparqlResponse.class);
- }
- }).doOnError(Timber::e));
- }
+ private Single> depictedItemsFrom(Request request) {
+ return depictsClient.toDepictions(Single.fromCallable(() -> {
+ try (ResponseBody body = okHttpClient.newCall(request).execute().body()) {
+ return gson.fromJson(body.string(), SparqlResponse.class);
+ }
+ }).doOnError(Timber::e));
+ }
- @NotNull
- private Request sparqlQuery(String qid, int startPosition, int limit, String fileName) throws IOException {
- String query = FileUtils.readFromResource(fileName)
- .replace("${QID}", qid)
- .replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\"")
- .replace("${LIMIT}",""+ limit)
- .replace("${OFFSET}",""+ startPosition);
- HttpUrl.Builder urlBuilder = HttpUrl
- .parse(sparqlQueryUrl)
- .newBuilder()
- .addQueryParameter("query", query)
- .addQueryParameter("format", "json");
- return new Request.Builder()
- .url(urlBuilder.build())
- .build();
- }
+ @NotNull
+ private Request sparqlQuery(String qid, int startPosition, int limit, String fileName)
+ throws IOException {
+ String query = FileUtils.readFromResource(fileName)
+ .replace("${QID}", qid)
+ .replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\"")
+ .replace("${LIMIT}", "" + limit)
+ .replace("${OFFSET}", "" + startPosition);
+ HttpUrl.Builder urlBuilder = HttpUrl
+ .parse(sparqlQueryUrl)
+ .newBuilder()
+ .addQueryParameter("query", query)
+ .addQueryParameter("format", "json");
+ return new Request.Builder()
+ .url(urlBuilder.build())
+ .build();
+ }
- 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;
- });
- }
+ 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;
+ });
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java
index a11906bdb..1ff2306ec 100644
--- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java
@@ -89,11 +89,6 @@ public class MoreBottomSheetLoggedOutFragment extends BottomSheetDialogFragment
getActivity().startActivity(intent);
}
- @OnClick(R.id.more_tutorial)
- public void onTutorialClicked() {
- WelcomeActivity.startYourself(getActivity());
- }
-
@OnClick(R.id.more_settings)
public void onSettingsClicked() {
final Intent intent = new Intent(getActivity(), SettingsActivity.class);
diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java
index f9699931a..134b3c672 100644
--- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java
+++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java
@@ -16,82 +16,81 @@ import org.wikipedia.model.EnumCodeMap;
import fr.free.nrw.commons.R;
-
public enum NavTab implements EnumCode {
- CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) {
+ CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) {
+ @NonNull
+ @Override
+ public Fragment newInstance() {
+ return ContributionsFragment.newInstance();
+ }
+ },
+ NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp) {
+ @NonNull
+ @Override
+ public Fragment newInstance() {
+ return NearbyParentFragment.newInstance();
+ }
+ },
+ EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
+ @NonNull
+ @Override
+ public Fragment newInstance() {
+ return ExploreFragment.newInstance();
+ }
+ },
+ FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) {
+ @NonNull
+ @Override
+ public Fragment newInstance() {
+ return BookmarkFragment.newInstance();
+ }
+ },
+ MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
+ @NonNull
+ @Override
+ public Fragment newInstance() {
+ return null;
+ }
+ };
+
+ private static final EnumCodeMap MAP = new EnumCodeMap<>(NavTab.class);
+
+ @StringRes
+ private final int text;
+ @DrawableRes
+ private final int icon;
+
@NonNull
- @Override
- public Fragment newInstance() {
- return ContributionsFragment.newInstance();
+ public static NavTab of(int code) {
+ return MAP.get(code);
}
- },
- NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp){
+
+ public static int size() {
+ return MAP.size();
+ }
+
+ @StringRes
+ public int text() {
+ return text;
+ }
+
+ @DrawableRes
+ public int icon() {
+ return icon;
+ }
+
@NonNull
+ public abstract Fragment newInstance();
+
@Override
- public Fragment newInstance() {
- return NearbyParentFragment.newInstance();
+ public int code() {
+ // This enumeration is not marshalled so tying declaration order to presentation order is
+ // convenient and consistent.
+ return ordinal();
}
- },
- EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
- @NonNull
- @Override
- public Fragment newInstance() {
- return ExploreFragment.newInstance();
+
+ NavTab(@StringRes int text, @DrawableRes int icon) {
+ this.text = text;
+ this.icon = icon;
}
- },
- FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) {
- @NonNull
- @Override
- public Fragment newInstance() {
- return BookmarkFragment.newInstance();
- }
- },
- MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
- @NonNull
- @Override
- public Fragment newInstance() {
- return null;
- }
- };
-
- private static final EnumCodeMap MAP = new EnumCodeMap<>(NavTab.class);
-
- @StringRes
- private final int text;
- @DrawableRes
- private final int icon;
-
- @NonNull
- public static NavTab of(int code) {
- return MAP.get(code);
- }
-
- public static int size() {
- return MAP.size();
- }
-
- @StringRes
- public int text() {
- return text;
- }
-
- @DrawableRes
- public int icon() {
- return icon;
- }
-
- @NonNull
- public abstract Fragment newInstance();
-
- @Override
- public int code() {
- // This enumeration is not marshalled so tying declaration order to presentation order is
- // convenient and consistent.
- return ordinal();
- }
-
- NavTab(@StringRes int text, @DrawableRes int icon) {
- this.text = text;
- this.icon = icon;
- }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java
index c725561e9..5384f2e01 100644
--- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java
@@ -8,28 +8,31 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
public class NavTabFragmentPagerAdapter extends FragmentPagerAdapter {
- private Fragment currentFragment;
- public NavTabFragmentPagerAdapter(FragmentManager mgr) {
- super(mgr);
- }
+ private Fragment currentFragment;
- @Nullable
- public Fragment getCurrentFragment() {
- return currentFragment;
- }
+ public NavTabFragmentPagerAdapter(FragmentManager mgr) {
+ super(mgr);
+ }
- @Override public Fragment getItem(int pos) {
- return NavTab.of(pos).newInstance();
- }
+ @Nullable
+ public Fragment getCurrentFragment() {
+ return currentFragment;
+ }
- @Override public int getCount() {
- return NavTab.size();
- }
+ @Override
+ public Fragment getItem(int pos) {
+ return NavTab.of(pos).newInstance();
+ }
- @Override
- public void setPrimaryItem(ViewGroup container, int position, Object object) {
- currentFragment = ((Fragment) object);
- super.setPrimaryItem(container, position, object);
- }
+ @Override
+ public int getCount() {
+ return NavTab.size();
+ }
+
+ @Override
+ public void setPrimaryItem(ViewGroup container, int position, Object object) {
+ currentFragment = ((Fragment) object);
+ super.setPrimaryItem(container, position, object);
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java
index b7cf63145..399cbc789 100644
--- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java
+++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java
@@ -10,32 +10,32 @@ import fr.free.nrw.commons.contributions.MainActivity;
public class NavTabLayout extends BottomNavigationView {
- public NavTabLayout(Context context) {
- super(context);
- setTabViews();
- }
-
- public NavTabLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- setTabViews();
- }
-
- public NavTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- setTabViews();
- }
-
- private void setTabViews() {
- if (((MainActivity)getContext()).applicationKvStore.getBoolean("login_skipped") == true) {
- for (int i = 0; i < NavTabLoggedOut.size(); i++) {
- NavTabLoggedOut navTab = NavTabLoggedOut.of(i);
- getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
- }
- } else {
- for (int i = 0; i < NavTab.size(); i++) {
- NavTab navTab = NavTab.of(i);
- getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
- }
+ public NavTabLayout(Context context) {
+ super(context);
+ setTabViews();
+ }
+
+ public NavTabLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setTabViews();
+ }
+
+ public NavTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setTabViews();
+ }
+
+ private void setTabViews() {
+ if (((MainActivity) getContext()).applicationKvStore.getBoolean("login_skipped") == true) {
+ for (int i = 0; i < NavTabLoggedOut.size(); i++) {
+ NavTabLoggedOut navTab = NavTabLoggedOut.of(i);
+ getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
+ }
+ } else {
+ for (int i = 0; i < NavTab.size(); i++) {
+ NavTab navTab = NavTab.of(i);
+ getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
+ }
+ }
}
- }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java
index 4f5324a9b..2ddf5e3e0 100644
--- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java
+++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java
@@ -13,66 +13,67 @@ import org.wikipedia.model.EnumCodeMap;
public enum NavTabLoggedOut implements EnumCode {
- EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
+ EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
+ @NonNull
+ @Override
+ public Fragment newInstance() {
+ return ExploreFragment.newInstance();
+ }
+ },
+ FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) {
+ @NonNull
+ @Override
+ public Fragment newInstance() {
+ return BookmarkFragment.newInstance();
+ }
+ },
+ MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
+ @NonNull
+ @Override
+ public Fragment newInstance() {
+ return null;
+ }
+ };
+
+ private static final EnumCodeMap MAP = new EnumCodeMap<>(
+ NavTabLoggedOut.class);
+
+ @StringRes
+ private final int text;
+ @DrawableRes
+ private final int icon;
+
@NonNull
- @Override
- public Fragment newInstance() {
- return ExploreFragment.newInstance();
+ public static NavTabLoggedOut of(int code) {
+ return MAP.get(code);
}
- },
- FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) {
+
+ public static int size() {
+ return MAP.size();
+ }
+
+ @StringRes
+ public int text() {
+ return text;
+ }
+
+ @DrawableRes
+ public int icon() {
+ return icon;
+ }
+
@NonNull
+ public abstract Fragment newInstance();
+
@Override
- public Fragment newInstance() {
- return BookmarkFragment.newInstance();
+ public int code() {
+ // This enumeration is not marshalled so tying declaration order to presentation order is
+ // convenient and consistent.
+ return ordinal();
}
- },
- MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
- @NonNull
- @Override
- public Fragment newInstance() {
- return null;
+
+ NavTabLoggedOut(@StringRes int text, @DrawableRes int icon) {
+ this.text = text;
+ this.icon = icon;
}
- };
-
- private static final EnumCodeMap MAP = new EnumCodeMap<>(NavTabLoggedOut.class);
-
- @StringRes
- private final int text;
- @DrawableRes
- private final int icon;
-
- @NonNull
- public static NavTabLoggedOut of(int code) {
- return MAP.get(code);
- }
-
- public static int size() {
- return MAP.size();
- }
-
- @StringRes
- public int text() {
- return text;
- }
-
- @DrawableRes
- public int icon() {
- return icon;
- }
-
- @NonNull
- public abstract Fragment newInstance();
-
- @Override
- public int code() {
- // This enumeration is not marshalled so tying declaration order to presentation order is
- // convenient and consistent.
- return ordinal();
- }
-
- NavTabLoggedOut(@StringRes int text, @DrawableRes int icon) {
- this.text = text;
- this.icon = icon;
- }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java
index 1767a12a4..842bca038 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java
@@ -222,6 +222,15 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
private PlaceAdapter adapter;
private NearbyParentFragmentInstanceReadyCallback nearbyParentFragmentInstanceReadyCallback;
+ /**
+ * Holds filtered markers that are to be shown
+ */
+ private List filteredMarkers;
+ /**
+ * Holds all the markers
+ */
+ private List allMarkers;
+
@NonNull
public static NearbyParentFragment newInstance() {
NearbyParentFragment fragment = new NearbyParentFragment();
@@ -1241,6 +1250,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
// Remove the previous markers before updating them
hideAllMarkers();
+ filteredMarkers = new ArrayList<>();
+
for (final MarkerPlaceGroup markerPlaceGroup : NearbyController.markerLabelList) {
final Place place = markerPlaceGroup.getPlace();
@@ -1272,6 +1283,9 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
updateMarker(markerPlaceGroup.getIsBookmarked(), place, NearbyController.currentLocation);
}
}
+
+ mapBox.clear();
+ mapBox.addMarkers(filteredMarkers);
}
@Override
@@ -1289,25 +1303,35 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(
getContext().getResources(), getIconFor(place, isBookmarked), getContext().getTheme());
- for (Marker marker : mapBox.getMarkers()) {
- if (marker.getTitle() != null && marker.getTitle().equals(place.getName())) {
+ if(curLatLng != null) {
+ for (NearbyBaseMarker nearbyMarker : allMarkers) {
+ if (nearbyMarker.getMarker().getTitle() != null && nearbyMarker.getMarker().getTitle().equals(place.getName())) {
+
+ final Bitmap icon = UiUtils.getBitmap(vectorDrawable);
- final Bitmap icon = UiUtils.getBitmap(vectorDrawable);
- if (curLatLng != null) {
final String distance = formatDistanceBetween(curLatLng, place.location);
place.setDistance(distance);
- }
- final NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
- nearbyBaseMarker.title(place.name);
- nearbyBaseMarker.position(
+ final NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
+ nearbyBaseMarker.title(place.name);
+ nearbyBaseMarker.position(
new com.mapbox.mapboxsdk.geometry.LatLng(
- place.location.getLatitude(),
- place.location.getLongitude()));
- nearbyBaseMarker.place(place);
- nearbyBaseMarker.icon(IconFactory.getInstance(getContext())
+ place.location.getLatitude(),
+ place.location.getLongitude()));
+ nearbyBaseMarker.place(place);
+ nearbyBaseMarker.icon(IconFactory.getInstance(getContext())
.fromBitmap(icon));
- marker.setIcon(IconFactory.getInstance(getContext()).fromBitmap(icon));
+ nearbyMarker.setIcon(IconFactory.getInstance(getContext()).fromBitmap(icon));
+ filteredMarkers.add(nearbyBaseMarker);
+ }
+ }
+ } else {
+ for (Marker marker : mapBox.getMarkers()) {
+ if (marker.getTitle() != null && marker.getTitle().equals(place.getName())) {
+
+ final Bitmap icon = UiUtils.getBitmap(vectorDrawable);
+ marker.setIcon(IconFactory.getInstance(getContext()).fromBitmap(icon));
+ }
}
}
}
@@ -1348,6 +1372,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
private void addNearbyMarkersToMapBoxMap(final List nearbyBaseMarkers, final Marker selectedMarker) {
if (isMapBoxReady && mapBox != null) {
+ allMarkers = new ArrayList<>(nearbyBaseMarkers);
mapBox.addMarkers(nearbyBaseMarkers);
setMapMarkerActions(selectedMarker);
presenter.updateMapMarkersToController(nearbyBaseMarkers);
diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java
index e81c06c3e..81ccbdb81 100644
--- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java
@@ -2,7 +2,7 @@ package fr.free.nrw.commons.quiz;
import android.content.Intent;
import android.os.Bundle;
-import android.widget.RadioButton;
+import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
@@ -26,14 +26,22 @@ public class QuizActivity extends AppCompatActivity {
@BindView(R.id.question_image) SimpleDraweeView imageView;
@BindView(R.id.question_text) TextView questionText;
@BindView(R.id.question_title) TextView questionTitle;
- @BindView(R.id.quiz_positive_answer) RadioButton positiveAnswer;
- @BindView(R.id.quiz_negative_answer) RadioButton negativeAnswer;
+ @BindView(R.id.quiz_positive_answer) Button positiveAnswer;
+ @BindView(R.id.quiz_negative_answer) Button negativeAnswer;
@BindView(R.id.toolbar) Toolbar toolbar;
private QuizController quizController = new QuizController();
private ArrayList quiz = new ArrayList<>();
private int questionIndex = 0;
private int score;
+ /**
+ * isPositiveAnswerChecked : represents yes click event
+ */
+ private boolean isPositiveAnswerChecked;
+ /**
+ * isNegativeAnswerChecked : represents no click event
+ */
+ private boolean isNegativeAnswerChecked;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -43,25 +51,23 @@ public class QuizActivity extends AppCompatActivity {
quizController.initialize(this);
ButterKnife.bind(this);
setSupportActionBar(toolbar);
+ positiveAnswer = findViewById(R.id.quiz_positive_answer);
+ negativeAnswer = findViewById(R.id.quiz_negative_answer);
displayQuestion();
}
/**
* to move to next question and check whether answer is selected or not
*/
- @OnClick(R.id.next_button)
public void setNextQuestion(){
- if ( questionIndex <= quiz.size() && (positiveAnswer.isChecked() || negativeAnswer.isChecked())) {
+ if ( questionIndex <= quiz.size() && (isPositiveAnswerChecked || isNegativeAnswerChecked)) {
evaluateScore();
- } else if ( !positiveAnswer.isChecked() && !negativeAnswer.isChecked()){
- AlertDialog.Builder alert = new AlertDialog.Builder(this);
- alert.setTitle(getResources().getString(R.string.warning));
- alert.setMessage(getResources().getString(R.string.warning_for_no_answer));
- alert.setPositiveButton(R.string.continue_message, (dialog, which) -> dialog.dismiss());
- AlertDialog dialog = alert.create();
- dialog.show();
}
+ }
+ @OnClick(R.id.next_button)
+ public void notKnowAnswer(){
+ customAlert("Information", quiz.get(questionIndex).getAnswerMessage());
}
/**
@@ -98,17 +104,24 @@ public class QuizActivity extends AppCompatActivity {
.build());
imageView.setImageURI(quiz.get(questionIndex).getUrl());
- new RadioGroupHelper(this, R.id.quiz_positive_answer, R.id.quiz_negative_answer);
- positiveAnswer.setChecked(false);
- negativeAnswer.setChecked(false);
+ isPositiveAnswerChecked = false;
+ isNegativeAnswerChecked = false;
+ positiveAnswer.setOnClickListener(view -> {
+ isPositiveAnswerChecked = true;
+ setNextQuestion();
+ });
+ negativeAnswer.setOnClickListener(view -> {
+ isNegativeAnswerChecked = true;
+ setNextQuestion();
+ });
}
/**
* to evaluate score and check whether answer is correct or wrong
*/
public void evaluateScore() {
- if ((quiz.get(questionIndex).isAnswer() && positiveAnswer.isChecked()) ||
- (!quiz.get(questionIndex).isAnswer() && negativeAnswer.isChecked()) ){
+ if ((quiz.get(questionIndex).isAnswer() && isPositiveAnswerChecked) ||
+ (!quiz.get(questionIndex).isAnswer() && isNegativeAnswerChecked) ){
customAlert(getResources().getString(R.string.correct),quiz.get(questionIndex).getAnswerMessage() );
score++;
} else {
diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java
index a470d163f..e9163cc6b 100644
--- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java
+++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java
@@ -19,8 +19,11 @@ import io.reactivex.Flowable;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -254,6 +257,23 @@ public class UploadRepository {
return depictModel.searchAllEntities(query);
}
+ /**
+ * Gets the depiction for each unique {@link Place} associated with an {@link UploadItem}
+ * from {@link #getUploads()}
+ *
+ * @return a single that provides the depictions
+ */
+ public Single> getPlaceDepictions() {
+ final Set places = new HashSet<>();
+ for (final UploadItem item : getUploads()) {
+ final Place place = item.getPlace();
+ if (place != null) {
+ places.add(place);
+ }
+ }
+ return depictModel.getPlaceDepictions(new ArrayList<>(places));
+ }
+
/**
* Returns nearest place matching the passed latitude and longitude
* @param decLatitude
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java
index d7f06155e..51bbd02bf 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java
@@ -20,65 +20,65 @@ import timber.log.Timber;
@Singleton
public class FileUtilsWrapper {
- @Inject
- public FileUtilsWrapper() {
+ @Inject
+ public FileUtilsWrapper() {
- }
-
- public String getFileExt(String fileName) {
- return FileUtils.getFileExt(fileName);
- }
-
- public String getSHA1(InputStream is) {
- return FileUtils.getSHA1(is);
- }
-
- public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
- return FileUtils.getFileInputStream(filePath);
- }
-
- public String getGeolocationOfFile(String filePath) {
- return FileUtils.getGeolocationOfFile(filePath);
- }
-
-
- /**
- * Takes a file as input and returns an Observable of files with the specified chunk size
- */
- public List getFileChunks(Context context, File file, final int chunkSize)
- throws IOException {
- final byte[] buffer = new byte[chunkSize];
-
- //try-with-resources to ensure closing stream
- try (final FileInputStream fis = new FileInputStream(file);
- final BufferedInputStream bis = new BufferedInputStream(fis)) {
- final List buffers = new ArrayList<>();
- int size;
- while ((size = bis.read(buffer)) > 0) {
- buffers.add(writeToFile(context, Arrays.copyOf(buffer, size), file.getName(),
- getFileExt(file.getName())));
- }
- return buffers;
}
- }
- /**
- * Create a temp file containing the passed byte data.
- */
- private File writeToFile(Context context, final byte[] data, final String fileName,
- String fileExtension)
- throws IOException {
- final File file = File.createTempFile(fileName, fileExtension, context.getCacheDir());
- try {
- if (!file.exists()) {
- file.createNewFile();
- }
- final FileOutputStream fos = new FileOutputStream(file);
- fos.write(data);
- fos.close();
- } catch (final Exception throwable) {
- Timber.e(throwable, "Failed to create file");
+ public String getFileExt(String fileName) {
+ return FileUtils.getFileExt(fileName);
+ }
+
+ public String getSHA1(InputStream is) {
+ return FileUtils.getSHA1(is);
+ }
+
+ public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
+ return FileUtils.getFileInputStream(filePath);
+ }
+
+ public String getGeolocationOfFile(String filePath) {
+ return FileUtils.getGeolocationOfFile(filePath);
+ }
+
+
+ /**
+ * Takes a file as input and returns an Observable of files with the specified chunk size
+ */
+ public List getFileChunks(Context context, File file, final int chunkSize)
+ throws IOException {
+ final byte[] buffer = new byte[chunkSize];
+
+ //try-with-resources to ensure closing stream
+ try (final FileInputStream fis = new FileInputStream(file);
+ final BufferedInputStream bis = new BufferedInputStream(fis)) {
+ final List buffers = new ArrayList<>();
+ int size;
+ while ((size = bis.read(buffer)) > 0) {
+ buffers.add(writeToFile(context, Arrays.copyOf(buffer, size), file.getName(),
+ getFileExt(file.getName())));
+ }
+ return buffers;
+ }
+ }
+
+ /**
+ * Create a temp file containing the passed byte data.
+ */
+ private File writeToFile(Context context, final byte[] data, final String fileName,
+ String fileExtension)
+ throws IOException {
+ final File file = File.createTempFile(fileName, fileExtension, context.getCacheDir());
+ try {
+ if (!file.exists()) {
+ file.createNewFile();
+ }
+ final FileOutputStream fos = new FileOutputStream(file);
+ fos.write(data);
+ fos.close();
+ } catch (final Exception throwable) {
+ Timber.e(throwable, "Failed to create file");
+ }
+ return file;
}
- return file;
- }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java
index e04afe979..1f31c1690 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java
@@ -22,6 +22,7 @@ import timber.log.Timber;
*/
@Singleton
public class ImageProcessingService {
+
private final FileUtilsWrapper fileUtilsWrapper;
private final ImageUtilsWrapper imageUtilsWrapper;
private final ReadFBMD readFBMD;
@@ -30,9 +31,9 @@ public class ImageProcessingService {
@Inject
public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper,
- ImageUtilsWrapper imageUtilsWrapper,
- ReadFBMD readFBMD, EXIFReader EXIFReader,
- MediaClient mediaClient, Context context) {
+ ImageUtilsWrapper imageUtilsWrapper,
+ ReadFBMD readFBMD, EXIFReader EXIFReader,
+ MediaClient mediaClient, Context context) {
this.fileUtilsWrapper = fileUtilsWrapper;
this.imageUtilsWrapper = imageUtilsWrapper;
this.readFBMD = readFBMD;
@@ -41,33 +42,34 @@ public class ImageProcessingService {
}
- /**
- * Check image quality before upload - checks duplicate image - checks dark image - checks
- * geolocation for image - check for valid title
- */
- Single validateImage(UploadItem uploadItem) {
- int currentImageQuality = uploadItem.getImageQuality();
- Timber.d("Current image quality is %d", currentImageQuality);
- if (currentImageQuality == ImageUtils.IMAGE_KEEP) {
- return Single.just(ImageUtils.IMAGE_OK);
- }
- Timber.d("Checking the validity of image");
- String filePath = uploadItem.getMediaUri().getPath();
-
- return Single.zip(
- checkDuplicateImage(filePath),
- checkImageGeoLocation(uploadItem.getPlace(), filePath),
- checkDarkImage(filePath),
- validateItemTitle(uploadItem),
- checkFBMD(filePath),
- checkEXIF(filePath),
- (duplicateImage, wrongGeoLocation, darkImage, itemTitle, fbmd, exif) -> {
- Timber.d("duplicate: %d, geo: %d, dark: %d, title: %d" + "fbmd:" + fbmd + "exif:" + exif,
- duplicateImage, wrongGeoLocation, darkImage, itemTitle);
- return duplicateImage | wrongGeoLocation | darkImage | itemTitle | fbmd | exif;
+ /**
+ * Check image quality before upload - checks duplicate image - checks dark image - checks
+ * geolocation for image - check for valid title
+ */
+ Single validateImage(UploadItem uploadItem) {
+ int currentImageQuality = uploadItem.getImageQuality();
+ Timber.d("Current image quality is %d", currentImageQuality);
+ if (currentImageQuality == ImageUtils.IMAGE_KEEP) {
+ return Single.just(ImageUtils.IMAGE_OK);
}
- );
- }
+ Timber.d("Checking the validity of image");
+ String filePath = uploadItem.getMediaUri().getPath();
+
+ return Single.zip(
+ checkDuplicateImage(filePath),
+ checkImageGeoLocation(uploadItem.getPlace(), filePath),
+ checkDarkImage(filePath),
+ validateItemTitle(uploadItem),
+ checkFBMD(filePath),
+ checkEXIF(filePath),
+ (duplicateImage, wrongGeoLocation, darkImage, itemTitle, fbmd, exif) -> {
+ Timber.d("duplicate: %d, geo: %d, dark: %d, title: %d" + "fbmd:" + fbmd + "exif:"
+ + exif,
+ duplicateImage, wrongGeoLocation, darkImage, itemTitle);
+ return duplicateImage | wrongGeoLocation | darkImage | itemTitle | fbmd | exif;
+ }
+ );
+ }
/**
* We want to discourage users from uploading images to Commons that were taken from Facebook.
@@ -79,10 +81,10 @@ public class ImageProcessingService {
}
/**
- * We try to minimize uploads from the Commons app that might be copyright violations.
- * If an image does not have any Exif metadata, then it was likely downloaded from the internet,
- * and is probably not an original work by the user. We detect these kinds of images by looking
- * for the presence of some basic Exif metadata.
+ * We try to minimize uploads from the Commons app that might be copyright violations. If an
+ * image does not have any Exif metadata, then it was likely downloaded from the internet, and
+ * is probably not an original work by the user. We detect these kinds of images by looking for
+ * the presence of some basic Exif metadata.
*/
private Single checkEXIF(String filepath) {
return EXIFReader.processMetadata(filepath);
@@ -90,9 +92,7 @@ public class ImageProcessingService {
/**
- * Checks item caption
- * - empty caption
- * - existing caption
+ * Checks item caption - empty caption - existing caption
*
* @param uploadItem
* @return
@@ -105,11 +105,11 @@ public class ImageProcessingService {
}
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName())
- .map(doesFileExist -> {
- Timber.d("Result for valid title is %s", doesFileExist);
- return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK;
- })
- .subscribeOn(Schedulers.io());
+ .map(doesFileExist -> {
+ Timber.d("Result for valid title is %s", doesFileExist);
+ return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK;
+ })
+ .subscribeOn(Schedulers.io());
}
/**
@@ -121,13 +121,13 @@ public class ImageProcessingService {
private Single checkDuplicateImage(String filePath) {
Timber.d("Checking for duplicate image %s", filePath);
return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath))
- .map(fileUtilsWrapper::getSHA1)
- .flatMap(mediaClient::checkFileExistsUsingSha)
- .map(b -> {
- Timber.d("Result for duplicate image %s", b);
- return b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK;
- })
- .subscribeOn(Schedulers.io());
+ .map(fileUtilsWrapper::getSHA1)
+ .flatMap(mediaClient::checkFileExistsUsingSha)
+ .map(b -> {
+ Timber.d("Result for duplicate image %s", b);
+ return b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK;
+ })
+ .subscribeOn(Schedulers.io());
}
/**
@@ -142,8 +142,8 @@ public class ImageProcessingService {
}
/**
- * Checks for image geolocation
- * returns IMAGE_OK if the place is null or if the file doesn't contain a geolocation
+ * Checks for image geolocation returns IMAGE_OK if the place is null or if the file doesn't
+ * contain a geolocation
*
* @param filePath file to be checked
* @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK
@@ -154,14 +154,15 @@ public class ImageProcessingService {
return Single.just(ImageUtils.IMAGE_OK);
}
return Single.fromCallable(() -> filePath)
- .map(fileUtilsWrapper::getGeolocationOfFile)
- .flatMap(geoLocation -> {
- if (StringUtils.isBlank(geoLocation)) {
- return Single.just(ImageUtils.IMAGE_OK);
- }
- return imageUtilsWrapper.checkImageGeolocationIsDifferent(geoLocation, place.getLocation());
- })
- .subscribeOn(Schedulers.io());
+ .map(fileUtilsWrapper::getGeolocationOfFile)
+ .flatMap(geoLocation -> {
+ if (StringUtils.isBlank(geoLocation)) {
+ return Single.just(ImageUtils.IMAGE_OK);
+ }
+ return imageUtilsWrapper
+ .checkImageGeolocationIsDifferent(geoLocation, place.getLocation());
+ })
+ .subscribeOn(Schedulers.io());
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.java b/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.java
index ddb629979..e344cf55d 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.java
@@ -16,96 +16,96 @@ import org.apache.commons.lang3.StringUtils;
class PageContentsCreator {
- //{{According to Exif data|2009-01-09}}
- private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}";
+ //{{According to Exif data|2009-01-09}}
+ private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}";
- //2009-01-09 → 9 January 2009
- private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s";
+ //2009-01-09 → 9 January 2009
+ private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s";
- private final Context context;
+ private final Context context;
- @Inject
- public PageContentsCreator(Context context) {
- this.context = context;
- }
-
- public String createFrom(Contribution contribution) {
- StringBuilder buffer = new StringBuilder();
- final Media media = contribution.getMedia();
- buffer
- .append("== {{int:filedesc}} ==\n")
- .append("{{Information\n")
- .append("|description=").append(media.getFallbackDescription()).append("\n")
- .append("|source=").append("{{own}}\n")
- .append("|author=[[User:").append(media.getAuthor()).append("|")
- .append(media.getAuthor()).append("]]\n");
-
- String templatizedCreatedDate = getTemplatizedCreatedDate(
- contribution.getDateCreated(), contribution.getDateCreatedSource());
- if (!StringUtils.isBlank(templatizedCreatedDate)) {
- buffer.append("|date=").append(templatizedCreatedDate);
+ @Inject
+ public PageContentsCreator(Context context) {
+ this.context = context;
}
- buffer.append("}}").append("\n");
+ public String createFrom(Contribution contribution) {
+ StringBuilder buffer = new StringBuilder();
+ final Media media = contribution.getMedia();
+ buffer
+ .append("== {{int:filedesc}} ==\n")
+ .append("{{Information\n")
+ .append("|description=").append(media.getFallbackDescription()).append("\n")
+ .append("|source=").append("{{own}}\n")
+ .append("|author=[[User:").append(media.getAuthor()).append("|")
+ .append(media.getAuthor()).append("]]\n");
- //Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null
- final String decimalCoords = contribution.getDecimalCoords();
- if (decimalCoords != null) {
- buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n");
+ String templatizedCreatedDate = getTemplatizedCreatedDate(
+ contribution.getDateCreated(), contribution.getDateCreatedSource());
+ if (!StringUtils.isBlank(templatizedCreatedDate)) {
+ buffer.append("|date=").append(templatizedCreatedDate);
+ }
+
+ buffer.append("}}").append("\n");
+
+ //Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null
+ final String decimalCoords = contribution.getDecimalCoords();
+ if (decimalCoords != null) {
+ buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n");
+ }
+
+ buffer.append("== {{int:license-header}} ==\n")
+ .append(licenseTemplateFor(media.getLicense())).append("\n\n")
+ .append("{{Uploaded from Mobile|platform=Android|version=")
+ .append(ConfigUtils.getVersionNameWithSha(context)).append("}}\n");
+ final List categories = media.getCategories();
+ if (categories != null && categories.size() != 0) {
+ for (int i = 0; i < categories.size(); i++) {
+ buffer.append("\n[[Category:").append(categories.get(i)).append("]]");
+ }
+ } else {
+ buffer.append("{{subst:unc}}");
+ }
+ return buffer.toString();
}
- buffer.append("== {{int:license-header}} ==\n")
- .append(licenseTemplateFor(media.getLicense())).append("\n\n")
- .append("{{Uploaded from Mobile|platform=Android|version=")
- .append(ConfigUtils.getVersionNameWithSha(context)).append("}}\n");
- final List categories = media.getCategories();
- if (categories != null && categories.size() != 0) {
- for (int i = 0; i < categories.size(); i++) {
- buffer.append("\n[[Category:").append(categories.get(i)).append("]]");
- }
- } else {
- buffer.append("{{subst:unc}}");
- }
- return buffer.toString();
- }
-
- /**
- * Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE
- *
- * @param dateCreated
- * @param dateCreatedSource
- * @return
- */
- private String getTemplatizedCreatedDate(Date dateCreated, String dateCreatedSource) {
- if (dateCreated != null) {
- SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
- return String.format(Locale.ENGLISH,
- isExif(dateCreatedSource) ? TEMPLATE_DATE_ACC_TO_EXIF : TEMPLATE_DATA_OTHER_SOURCE,
- dateFormat.format(dateCreated)
- ) + "\n";
- }
- return "";
- }
-
- private boolean isExif(String dateCreatedSource) {
- return DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource);
- }
-
- @NonNull
- private String licenseTemplateFor(String license) {
- switch (license) {
- case Licenses.CC_BY_3:
- return "{{self|cc-by-3.0}}";
- case Licenses.CC_BY_4:
- return "{{self|cc-by-4.0}}";
- case Licenses.CC_BY_SA_3:
- return "{{self|cc-by-sa-3.0}}";
- case Licenses.CC_BY_SA_4:
- return "{{self|cc-by-sa-4.0}}";
- case Licenses.CC0:
- return "{{self|cc-zero}}";
+ /**
+ * Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE
+ *
+ * @param dateCreated
+ * @param dateCreatedSource
+ * @return
+ */
+ private String getTemplatizedCreatedDate(Date dateCreated, String dateCreatedSource) {
+ if (dateCreated != null) {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ return String.format(Locale.ENGLISH,
+ isExif(dateCreatedSource) ? TEMPLATE_DATE_ACC_TO_EXIF : TEMPLATE_DATA_OTHER_SOURCE,
+ dateFormat.format(dateCreated)
+ ) + "\n";
+ }
+ return "";
}
- throw new RuntimeException("Unrecognized license value: " + license);
- }
+ private boolean isExif(String dateCreatedSource) {
+ return DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource);
+ }
+
+ @NonNull
+ private String licenseTemplateFor(String license) {
+ switch (license) {
+ case Licenses.CC_BY_3:
+ return "{{self|cc-by-3.0}}";
+ case Licenses.CC_BY_4:
+ return "{{self|cc-by-4.0}}";
+ case Licenses.CC_BY_SA_3:
+ return "{{self|cc-by-sa-3.0}}";
+ case Licenses.CC_BY_SA_4:
+ return "{{self|cc-by-sa-4.0}}";
+ case Licenses.CC0:
+ return "{{self|cc-zero}}";
+ }
+
+ throw new RuntimeException("Unrecognized license value: " + license);
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.java b/app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.java
index e98ab9ec5..586f9fc24 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.java
@@ -15,34 +15,34 @@ import javax.inject.Singleton;
@Singleton
public class ReadFBMD {
- @Inject
- public ReadFBMD() {
- }
+ @Inject
+ public ReadFBMD() {
+ }
- public Single processMetadata(String path) {
- return Single.fromCallable(() -> {
- try {
- int psBlockOffset;
- int fbmdOffset;
+ public Single processMetadata(String path) {
+ return Single.fromCallable(() -> {
+ try {
+ int psBlockOffset;
+ int fbmdOffset;
- try (FileInputStream fs = new FileInputStream(path)) {
- byte[] bytes = new byte[4096];
- fs.read(bytes);
- fs.close();
- String fileStr = new String(bytes);
- psBlockOffset = fileStr.indexOf("8BIM");
- fbmdOffset = fileStr.indexOf("FBMD");
- }
+ try (FileInputStream fs = new FileInputStream(path)) {
+ byte[] bytes = new byte[4096];
+ fs.read(bytes);
+ fs.close();
+ String fileStr = new String(bytes);
+ psBlockOffset = fileStr.indexOf("8BIM");
+ fbmdOffset = fileStr.indexOf("FBMD");
+ }
- if (psBlockOffset > 0 && fbmdOffset > 0
- && fbmdOffset > psBlockOffset && fbmdOffset - psBlockOffset < 0x80) {
- return ImageUtils.FILE_FBMD;
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- return ImageUtils.IMAGE_OK;
- });
- }
+ if (psBlockOffset > 0 && fbmdOffset > 0
+ && fbmdOffset > psBlockOffset && fbmdOffset - psBlockOffset < 0x80) {
+ return ImageUtils.FILE_FBMD;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return ImageUtils.IMAGE_OK;
+ });
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java
index 2212e2b5b..b363caf99 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java
@@ -35,199 +35,204 @@ import timber.log.Timber;
@Singleton
public class UploadClient {
- private final int CHUNK_SIZE = 512 * 1024; // 512 KB
+ private final int CHUNK_SIZE = 512 * 1024; // 512 KB
- //This is maximum duration for which a stash is persisted on MediaWiki
- // https://www.mediawiki.org/wiki/Manual:$wgUploadStashMaxAge
- private final int MAX_CHUNK_AGE = 6 * 3600 * 1000; // 6 hours
+ //This is maximum duration for which a stash is persisted on MediaWiki
+ // https://www.mediawiki.org/wiki/Manual:$wgUploadStashMaxAge
+ private final int MAX_CHUNK_AGE = 6 * 3600 * 1000; // 6 hours
- private final UploadInterface uploadInterface;
- private final CsrfTokenClient csrfTokenClient;
- private final PageContentsCreator pageContentsCreator;
- private final FileUtilsWrapper fileUtilsWrapper;
- private final Gson gson;
+ private final UploadInterface uploadInterface;
+ private final CsrfTokenClient csrfTokenClient;
+ private final PageContentsCreator pageContentsCreator;
+ private final FileUtilsWrapper fileUtilsWrapper;
+ private final Gson gson;
- private final CompositeDisposable compositeDisposable = new CompositeDisposable();
+ private final CompositeDisposable compositeDisposable = new CompositeDisposable();
- @Inject
- public UploadClient(final UploadInterface uploadInterface,
- @Named(NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient,
- final PageContentsCreator pageContentsCreator,
- final FileUtilsWrapper fileUtilsWrapper, final Gson gson) {
- this.uploadInterface = uploadInterface;
- this.csrfTokenClient = csrfTokenClient;
- this.pageContentsCreator = pageContentsCreator;
- this.fileUtilsWrapper = fileUtilsWrapper;
- this.gson = gson;
- }
-
- /**
- * Upload file to stash in chunks of specified size. Uploading files in chunks will make handling
- * of large files easier. Also, it will be useful in supporting pause/resume of uploads
- */
- public Observable uploadFileToStash(
- final Context context, final String filename, final Contribution contribution,
- final NotificationUpdateProgressListener notificationUpdater) throws IOException {
- if (contribution.getChunkInfo() != null
- && contribution.getChunkInfo().getTotalChunks() == contribution.getChunkInfo()
- .getIndexOfNextChunkToUpload()) {
- return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
- contribution.getChunkInfo().getUploadResult().getFilekey()));
+ @Inject
+ public UploadClient(final UploadInterface uploadInterface,
+ @Named(NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient,
+ final PageContentsCreator pageContentsCreator,
+ final FileUtilsWrapper fileUtilsWrapper, final Gson gson) {
+ this.uploadInterface = uploadInterface;
+ this.csrfTokenClient = csrfTokenClient;
+ this.pageContentsCreator = pageContentsCreator;
+ this.fileUtilsWrapper = fileUtilsWrapper;
+ this.gson = gson;
}
- CommonsApplication.pauseUploads.put(contribution.getPageId(), false);
+ /**
+ * Upload file to stash in chunks of specified size. Uploading files in chunks will make
+ * handling of large files easier. Also, it will be useful in supporting pause/resume of
+ * uploads
+ */
+ public Observable uploadFileToStash(
+ final Context context, final String filename, final Contribution contribution,
+ final NotificationUpdateProgressListener notificationUpdater) throws IOException {
+ if (contribution.getChunkInfo() != null
+ && contribution.getChunkInfo().getTotalChunks() == contribution.getChunkInfo()
+ .getIndexOfNextChunkToUpload()) {
+ return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
+ contribution.getChunkInfo().getUploadResult().getFilekey()));
+ }
- final File file = new File(contribution.getLocalUri().getPath());
- final List fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
+ CommonsApplication.pauseUploads.put(contribution.getPageId(), false);
- final int totalChunks = fileChunks.size();
+ final File file = new File(contribution.getLocalUri().getPath());
+ final List fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
- final MediaType mediaType = MediaType
- .parse(FileUtils.getMimeType(context, Uri.parse(file.getPath())));
+ final int totalChunks = fileChunks.size();
- final AtomicReference chunkInfo = new AtomicReference<>();
- if (isStashValid(contribution)) {
- chunkInfo.set(contribution.getChunkInfo());
+ final MediaType mediaType = MediaType
+ .parse(FileUtils.getMimeType(context, Uri.parse(file.getPath())));
- Timber.d("Chunk: Next Chunk: %s, Total Chunks: %s",
- contribution.getChunkInfo().getIndexOfNextChunkToUpload(),
- contribution.getChunkInfo().getTotalChunks());
- }
+ final AtomicReference chunkInfo = new AtomicReference<>();
+ if (isStashValid(contribution)) {
+ chunkInfo.set(contribution.getChunkInfo());
- final AtomicInteger index = new AtomicInteger();
- final AtomicBoolean failures = new AtomicBoolean();
+ Timber.d("Chunk: Next Chunk: %s, Total Chunks: %s",
+ contribution.getChunkInfo().getIndexOfNextChunkToUpload(),
+ contribution.getChunkInfo().getTotalChunks());
+ }
- compositeDisposable.add(Observable.fromIterable(fileChunks).forEach(chunkFile -> {
- if (CommonsApplication.pauseUploads.get(contribution.getPageId()) || failures.get()) {
- return;
- }
+ final AtomicInteger index = new AtomicInteger();
+ final AtomicBoolean failures = new AtomicBoolean();
- if (chunkInfo.get() != null && index.get() < chunkInfo.get().getIndexOfNextChunkToUpload()) {
- index.incrementAndGet();
- Timber.d("Chunk: Increment and return: %s", index.get());
- return;
- }
- index.getAndIncrement();
- final int offset =
- chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getOffset() : 0;
-
- Timber.d("Chunk: Sending Chunk number: %s, offset: %s", index.get(), offset);
- final String filekey =
- chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getFilekey() : null;
-
- final RequestBody requestBody = RequestBody
- .create(mediaType, chunkFile);
- final CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
- notificationUpdater::onProgress, offset,
- file.length());
-
- compositeDisposable.add(uploadChunkToStash(filename,
- file.length(),
- offset,
- filekey,
- countingRequestBody).subscribe(uploadResult -> {
- Timber.d("Chunk: Received Chunk number: %s, offset: %s", index.get(),
- uploadResult.getOffset());
- chunkInfo.set(
- new ChunkInfo(uploadResult, index.get(), totalChunks));
- notificationUpdater.onChunkUploaded(contribution, chunkInfo.get());
- }, throwable -> {
- Timber.e(throwable, "Received error in chunk upload");
- failures.set(true);
- }));
- }));
-
- if (CommonsApplication.pauseUploads.get(contribution.getPageId())) {
- Timber.d("Upload stash paused %s", contribution.getPageId());
- return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null));
- } else if (failures.get()) {
- Timber.d("Upload stash contains failures %s", contribution.getPageId());
- return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
- } else if (chunkInfo.get() != null) {
- Timber.d("Upload stash success %s", contribution.getPageId());
- return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
- chunkInfo.get().getUploadResult().getFilekey()));
- } else {
- Timber.d("Upload stash failed %s", contribution.getPageId());
- return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
- }
- }
-
- /**
- * Stash is valid for 6 hours. This function checks the validity of stash
- * @param contribution
- * @return
- */
- private boolean isStashValid(Contribution contribution) {
- return contribution.getChunkInfo() != null &&
- contribution.getDateModified()
- .after(new Date(System.currentTimeMillis() - MAX_CHUNK_AGE));
- }
-
- /**
- * Uploads a file chunk to stash
- *
- * @param filename The name of the file being uploaded
- * @param fileSize The total size of the file
- * @param offset The offset returned by the previous chunk upload
- * @param fileKey The filekey returned by the previous chunk upload
- * @param countingRequestBody Request body with chunk file
- * @return
- */
- Observable uploadChunkToStash(final String filename,
- final long fileSize,
- final long offset,
- final String fileKey,
- final CountingRequestBody countingRequestBody) {
- final MultipartBody.Part filePart;
- try {
- filePart = MultipartBody.Part
- .createFormData("chunk", URLEncoder.encode(filename, "utf-8"), countingRequestBody);
-
- return uploadInterface.uploadFileToStash(toRequestBody(filename),
- toRequestBody(String.valueOf(fileSize)),
- toRequestBody(String.valueOf(offset)),
- toRequestBody(fileKey),
- toRequestBody(csrfTokenClient.getTokenBlocking()),
- filePart)
- .map(UploadResponse::getUpload);
- } catch (final Throwable throwable) {
- Timber.e(throwable, "Failed to upload chunk to stash");
- return Observable.error(throwable);
- }
- }
-
- /**
- * Converts string value to request body
- */
- @Nullable
- private RequestBody toRequestBody(@Nullable final String value) {
- return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value);
- }
-
-
- public Observable uploadFileFromStash(
- final Contribution contribution,
- final String uniqueFileName,
- final String fileKey) {
- try {
- return uploadInterface
- .uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
- pageContentsCreator.createFrom(contribution),
- CommonsApplication.DEFAULT_EDIT_SUMMARY,
- uniqueFileName,
- fileKey).map(uploadResponse -> {
- UploadResponse uploadResult = gson.fromJson(uploadResponse, UploadResponse.class);
- if (uploadResult.getUpload() == null) {
- final MwException exception = gson.fromJson(uploadResponse, MwException.class);
- Timber.e(exception, "Error in uploading file from stash");
- throw new RuntimeException(exception.getErrorCode());
+ compositeDisposable.add(Observable.fromIterable(fileChunks).forEach(chunkFile -> {
+ if (CommonsApplication.pauseUploads.get(contribution.getPageId()) || failures.get()) {
+ return;
}
- return uploadResult.getUpload();
- });
- } catch (final Throwable throwable) {
- Timber.e(throwable, "Exception occurred in uploading file from stash");
- return Observable.error(throwable);
+
+ if (chunkInfo.get() != null && index.get() < chunkInfo.get()
+ .getIndexOfNextChunkToUpload()) {
+ index.incrementAndGet();
+ Timber.d("Chunk: Increment and return: %s", index.get());
+ return;
+ }
+ index.getAndIncrement();
+ final int offset =
+ chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getOffset() : 0;
+
+ Timber.d("Chunk: Sending Chunk number: %s, offset: %s", index.get(), offset);
+ final String filekey =
+ chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getFilekey() : null;
+
+ final RequestBody requestBody = RequestBody
+ .create(mediaType, chunkFile);
+ final CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
+ notificationUpdater::onProgress, offset,
+ file.length());
+
+ compositeDisposable.add(uploadChunkToStash(filename,
+ file.length(),
+ offset,
+ filekey,
+ countingRequestBody).subscribe(uploadResult -> {
+ Timber.d("Chunk: Received Chunk number: %s, offset: %s", index.get(),
+ uploadResult.getOffset());
+ chunkInfo.set(
+ new ChunkInfo(uploadResult, index.get(), totalChunks));
+ notificationUpdater.onChunkUploaded(contribution, chunkInfo.get());
+ }, throwable -> {
+ Timber.e(throwable, "Received error in chunk upload");
+ failures.set(true);
+ }));
+ }));
+
+ if (CommonsApplication.pauseUploads.get(contribution.getPageId())) {
+ Timber.d("Upload stash paused %s", contribution.getPageId());
+ return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null));
+ } else if (failures.get()) {
+ Timber.d("Upload stash contains failures %s", contribution.getPageId());
+ return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
+ } else if (chunkInfo.get() != null) {
+ Timber.d("Upload stash success %s", contribution.getPageId());
+ return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
+ chunkInfo.get().getUploadResult().getFilekey()));
+ } else {
+ Timber.d("Upload stash failed %s", contribution.getPageId());
+ return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
+ }
+ }
+
+ /**
+ * Stash is valid for 6 hours. This function checks the validity of stash
+ *
+ * @param contribution
+ * @return
+ */
+ private boolean isStashValid(Contribution contribution) {
+ return contribution.getChunkInfo() != null &&
+ contribution.getDateModified()
+ .after(new Date(System.currentTimeMillis() - MAX_CHUNK_AGE));
+ }
+
+ /**
+ * Uploads a file chunk to stash
+ *
+ * @param filename The name of the file being uploaded
+ * @param fileSize The total size of the file
+ * @param offset The offset returned by the previous chunk upload
+ * @param fileKey The filekey returned by the previous chunk upload
+ * @param countingRequestBody Request body with chunk file
+ * @return
+ */
+ Observable uploadChunkToStash(final String filename,
+ final long fileSize,
+ final long offset,
+ final String fileKey,
+ final CountingRequestBody countingRequestBody) {
+ final MultipartBody.Part filePart;
+ try {
+ filePart = MultipartBody.Part
+ .createFormData("chunk", URLEncoder.encode(filename, "utf-8"), countingRequestBody);
+
+ return uploadInterface.uploadFileToStash(toRequestBody(filename),
+ toRequestBody(String.valueOf(fileSize)),
+ toRequestBody(String.valueOf(offset)),
+ toRequestBody(fileKey),
+ toRequestBody(csrfTokenClient.getTokenBlocking()),
+ filePart)
+ .map(UploadResponse::getUpload);
+ } catch (final Throwable throwable) {
+ Timber.e(throwable, "Failed to upload chunk to stash");
+ return Observable.error(throwable);
+ }
+ }
+
+ /**
+ * Converts string value to request body
+ */
+ @Nullable
+ private RequestBody toRequestBody(@Nullable final String value) {
+ return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value);
+ }
+
+
+ public Observable uploadFileFromStash(
+ final Contribution contribution,
+ final String uniqueFileName,
+ final String fileKey) {
+ try {
+ return uploadInterface
+ .uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
+ pageContentsCreator.createFrom(contribution),
+ CommonsApplication.DEFAULT_EDIT_SUMMARY,
+ uniqueFileName,
+ fileKey).map(uploadResponse -> {
+ UploadResponse uploadResult = gson
+ .fromJson(uploadResponse, UploadResponse.class);
+ if (uploadResult.getUpload() == null) {
+ final MwException exception = gson
+ .fromJson(uploadResponse, MwException.class);
+ Timber.e(exception, "Error in uploading file from stash");
+ throw new RuntimeException(exception.getErrorCode());
+ }
+ return uploadResult.getUpload();
+ });
+ } catch (final Throwable throwable) {
+ Timber.e(throwable, "Exception occurred in uploading file from stash");
+ return Observable.error(throwable);
+ }
}
- }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java
index 4d9d341b5..fd5dda9d8 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java
@@ -13,17 +13,17 @@ import java.util.Collections;
import java.util.List;
public class UploadItem {
-
- private final Uri mediaUri;
- private final String mimeType;
- private ImageCoordinates gpsCoords;
- private List uploadMediaDetails;
- private Place place;
- private final long createdTimestamp;
- private final String createdTimestampSource;
- private final BehaviorSubject imageQuality;
- private boolean hasInvalidLocation;
- private final Uri contentUri;
+
+ private final Uri mediaUri;
+ private final String mimeType;
+ private ImageCoordinates gpsCoords;
+ private List uploadMediaDetails;
+ private Place place;
+ private final long createdTimestamp;
+ private final String createdTimestampSource;
+ private final BehaviorSubject imageQuality;
+ private boolean hasInvalidLocation;
+ private final Uri contentUri;
@SuppressLint("CheckResult")
@@ -45,29 +45,29 @@ public class UploadItem {
imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
}
- public String getCreatedTimestampSource() {
- return createdTimestampSource;
- }
+ public String getCreatedTimestampSource() {
+ return createdTimestampSource;
+ }
- public ImageCoordinates getGpsCoords() {
- return gpsCoords;
- }
+ public ImageCoordinates getGpsCoords() {
+ return gpsCoords;
+ }
- public List getUploadMediaDetails() {
- return uploadMediaDetails;
- }
+ public List getUploadMediaDetails() {
+ return uploadMediaDetails;
+ }
- public long getCreatedTimestamp() {
- return createdTimestamp;
- }
+ public long getCreatedTimestamp() {
+ return createdTimestamp;
+ }
- public Uri getMediaUri() {
- return mediaUri;
- }
+ public Uri getMediaUri() {
+ return mediaUri;
+ }
- public int getImageQuality() {
- return imageQuality.getValue();
- }
+ public int getImageQuality() {
+ return imageQuality.getValue();
+ }
public Uri getContentUri() { return contentUri; }
@@ -75,54 +75,55 @@ public class UploadItem {
this.imageQuality.onNext(imageQuality);
}
- /**
- * Sets the corresponding place to the uploadItem
- * @param place geolocated Wikidata item
- */
- public void setPlace(Place place) {
- this.place = place;
- }
-
- public Place getPlace() {
- return place;
- }
-
- public void setMediaDetails(final List uploadMediaDetails) {
- this.uploadMediaDetails = uploadMediaDetails;
- }
-
- @Override
- public boolean equals(@Nullable final Object obj) {
- if (!(obj instanceof UploadItem)) {
- return false;
+ /**
+ * Sets the corresponding place to the uploadItem
+ *
+ * @param place geolocated Wikidata item
+ */
+ public void setPlace(Place place) {
+ this.place = place;
}
- return mediaUri.toString().contains(((UploadItem) (obj)).mediaUri.toString());
- }
+ public Place getPlace() {
+ return place;
+ }
- @Override
- public int hashCode() {
- return mediaUri.hashCode();
- }
+ public void setMediaDetails(final List uploadMediaDetails) {
+ this.uploadMediaDetails = uploadMediaDetails;
+ }
- /**
- * Choose a filename for the media. Currently, the caption is used as a filename. If several
- * languages have been entered, the first language is used.
- */
- public String getFileName() {
- return Utils.fixExtension(uploadMediaDetails.get(0).getCaptionText(),
- MimeTypeMapWrapper.getExtensionFromMimeType(mimeType));
- }
+ @Override
+ public boolean equals(@Nullable final Object obj) {
+ if (!(obj instanceof UploadItem)) {
+ return false;
+ }
+ return mediaUri.toString().contains(((UploadItem) (obj)).mediaUri.toString());
- public void setGpsCoords(final ImageCoordinates gpsCoords) {
- this.gpsCoords = gpsCoords;
- }
+ }
- public void setHasInvalidLocation(boolean hasInvalidLocation) {
- this.hasInvalidLocation=hasInvalidLocation;
- }
+ @Override
+ public int hashCode() {
+ return mediaUri.hashCode();
+ }
- public boolean hasInvalidLocation() {
- return hasInvalidLocation;
- }
+ /**
+ * Choose a filename for the media. Currently, the caption is used as a filename. If several
+ * languages have been entered, the first language is used.
+ */
+ public String getFileName() {
+ return Utils.fixExtension(uploadMediaDetails.get(0).getCaptionText(),
+ MimeTypeMapWrapper.getExtensionFromMimeType(mimeType));
+ }
+
+ public void setGpsCoords(final ImageCoordinates gpsCoords) {
+ this.gpsCoords = gpsCoords;
+ }
+
+ public void setHasInvalidLocation(boolean hasInvalidLocation) {
+ this.hasInvalidLocation = hasInvalidLocation;
+ }
+
+ public boolean hasInvalidLocation() {
+ return hasInvalidLocation;
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.java b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.java
index 5f215c3ad..0184744f8 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.java
@@ -60,6 +60,11 @@ public interface DepictsContract {
*/
void searchForDepictions(String query);
+ /**
+ * Selects all associated places (if any) as depictions
+ */
+ void selectPlaceDepictions();
+
/**
* Check if depictions were selected
* from the depiction list
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java
index 8b53d95a1..0b1dc5156 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java
@@ -100,6 +100,14 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra
depictsRecyclerView.setAdapter(adapter);
}
+ @Override
+ protected void onBecameVisible() {
+ super.onBecameVisible();
+ // Select Place depiction as the fragment becomes visible to ensure that the most up to date
+ // Place is used (i.e. if the user accepts a nearby place dialog)
+ presenter.selectPlaceDepictions();
+ }
+
@Override
public void goToNextScreen() {
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
@@ -146,6 +154,7 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra
@Override
public void setDepictsList(List depictedItemList) {
adapter.setItems(depictedItemList);
+ depictsRecyclerView.smoothScrollToPosition(0);
}
@OnClick(R.id.depicts_next)
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt
index a273aa57f..388dfeccf 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt
@@ -84,6 +84,35 @@ class DepictsPresenter @Inject constructor(
compositeDisposable.clear()
}
+ /**
+ * Selects the place depictions retrieved by the repository
+ */
+ override fun selectPlaceDepictions() {
+ compositeDisposable.add(repository.placeDepictions
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .subscribe(::selectNewDepictions)
+ )
+ }
+
+ /**
+ * Selects each [DepictedItem] in a given list as if they were clicked by the user by calling
+ * [onDepictItemClicked] for each depiction and adding the depictions to [depictedItems]
+ */
+ private fun selectNewDepictions(toSelect: List) {
+ toSelect.forEach {
+ it.isSelected = true
+ repository.onDepictItemClicked(it)
+ }
+
+ // Add the new selections to the list of depicted items so that the selections appear
+ // immediately (i.e. without any search term queries)
+ depictedItems.value?.toMutableList()
+ ?.let { toSelect + it }
+ ?.distinctBy(DepictedItem::id)
+ ?.let { depictedItems.value = it }
+ }
+
override fun onPreviousButtonClicked() {
view.goToPreviousScreen()
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/UploadDepictsAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/UploadDepictsAdapter.kt
index a3a843afa..d2c1bca7b 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/UploadDepictsAdapter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/UploadDepictsAdapter.kt
@@ -6,5 +6,6 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
class UploadDepictsAdapter(onDepictsClicked: (DepictedItem) -> Unit) :
BaseDelegateAdapter(
uploadDepictsDelegate(onDepictsClicked),
- areItemsTheSame = { oldItem, newItem -> oldItem.id == newItem.id }
+ areItemsTheSame = { oldItem, newItem -> oldItem.id == newItem.id },
+ areContentsTheSame = { itemA, itemB -> itemA.isSelected == itemB.isSelected}
)
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java
index 818962c2e..b73553f5e 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java
@@ -1,8 +1,10 @@
package fr.free.nrw.commons.upload.mediaDetails;
+import static android.app.Activity.RESULT_OK;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
import android.annotation.SuppressLint;
+import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -23,19 +25,19 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.github.chrisbanes.photoview.PhotoView;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import fr.free.nrw.commons.LocationPicker.LocationPicker;
import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore;
-import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.ImageCoordinates;
import fr.free.nrw.commons.upload.SimilarImageDialogFragment;
import fr.free.nrw.commons.upload.UploadBaseFragment;
+import fr.free.nrw.commons.upload.UploadItem;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter;
-import fr.free.nrw.commons.upload.UploadItem;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ViewUtil;
@@ -49,6 +51,7 @@ import timber.log.Timber;
public class UploadMediaDetailFragment extends UploadBaseFragment implements
UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener {
+ private static final int REQUEST_CODE = 1211;
@BindView(R.id.tv_title)
TextView tvTitle;
@BindView(R.id.ib_map)
@@ -94,7 +97,10 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
*/
private Place nearbyPlace;
private UploadItem uploadItem;
-
+ /**
+ * editableUploadItem : Storing the upload item before going to update the coordinates
+ */
+ private UploadItem editableUploadItem;
private UploadMediaDetailFragmentCallback callback;
@@ -378,10 +384,67 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
}
@Override
- public void showExternalMap(UploadItem uploadItem) {
- Utils.handleGeoCoordinates(getContext(),
- new LatLng(uploadItem.getGpsCoords().getDecLatitude(),
- uploadItem.getGpsCoords().getDecLongitude(), 0.0f));
+ public void showExternalMap(final UploadItem uploadItem) {
+ goToLocationPickerActivity(uploadItem);
+ }
+
+ /**
+ * Start Location picker activity. Show the location first then user can modify it by clicking
+ * modify location button.
+ * @param uploadItem current upload item
+ */
+ private void goToLocationPickerActivity(final UploadItem uploadItem) {
+
+ editableUploadItem = uploadItem;
+ startActivityForResult(new LocationPicker.IntentBuilder()
+ .defaultLocation(new CameraPosition.Builder()
+ .target(new com.mapbox.mapboxsdk.geometry.LatLng(uploadItem.getGpsCoords()
+ .getDecLatitude(),
+ uploadItem.getGpsCoords().getDecLongitude()))
+ .zoom(16).build())
+ .activityKey("UploadActivity")
+ .build(getActivity()), REQUEST_CODE);
+ }
+
+ /**
+ * Get the coordinates and update the existing coordinates.
+ * @param requestCode code of request
+ * @param resultCode code of result
+ * @param data intent
+ */
+ @Override
+ public void onActivityResult(final int requestCode, final int resultCode,
+ @Nullable final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
+
+ assert data != null;
+ final CameraPosition cameraPosition = LocationPicker.getCameraPosition(data);
+
+ if (cameraPosition != null) {
+
+ final String latitude = String.valueOf(cameraPosition.target.getLatitude());
+ final String longitude = String.valueOf(cameraPosition.target.getLongitude());
+
+ editLocation(latitude, longitude);
+ }
+ }
+ }
+
+ /**
+ * Update the old coordinates with new one
+ * @param latitude new latitude
+ * @param longitude new longitude
+ */
+ public void editLocation(final String latitude, final String longitude){
+
+ editableUploadItem.getGpsCoords().setDecLatitude(Double.parseDouble(latitude));
+ editableUploadItem.getGpsCoords().setDecLongitude(Double.parseDouble(longitude));
+ editableUploadItem.getGpsCoords().setDecimalCoords(latitude+"|"+longitude);
+ editableUploadItem.getGpsCoords().setImageCoordsExists(true);
+ Toast.makeText(getContext(), "Location Updated", Toast.LENGTH_LONG).show();
+
}
@Override
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt
index f2d48d5d6..6e4157c23 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt
@@ -3,6 +3,7 @@ package fr.free.nrw.commons.upload.structure.depictions
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.nearby.Place
import io.reactivex.Flowable
+import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.processors.BehaviorProcessor
import timber.log.Timber
@@ -27,19 +28,29 @@ class DepictModel @Inject constructor(private val depictsClient: DepictsClient)
fun searchAllEntities(query: String): Flowable> {
return if (query.isBlank())
nearbyPlaces.switchMap { places: List ->
- depictsClient.getEntities(places.toIds())
- .map {
- it.entities()
- .values
- .mapIndexed { index, entity -> DepictedItem(entity, places[index]) }
- }
- .onErrorResumeWithEmptyList()
- .toFlowable()
+ getPlaceDepictions(places).toFlowable()
}
else
networkItems(query)
}
+ /**
+ * Provides [DepictedItem] instances via a [Single] for a given list of [Place], providing an
+ * empty list if no places are provided or if there is an error
+ */
+ fun getPlaceDepictions(places: List): Single> =
+ places.toIds().let { ids ->
+ if (ids.isNotEmpty())
+ depictsClient.getEntities(ids)
+ .map{
+ it.entities()
+ .values
+ .mapIndexed { index, entity -> DepictedItem(entity, places[index])}
+ }
+ .onErrorResumeWithEmptyList()
+ else Single.just(emptyList())
+ }
+
private fun networkItems(query: String): Flowable> {
return depictsClient.searchForDepictions(query, SEARCH_DEPICTS_LIMIT, 0)
.onErrorResumeWithEmptyList()
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt
index 9320be156..5c1ee6846 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt
@@ -6,6 +6,7 @@ import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker
+import androidx.work.Data
import androidx.work.WorkerParameters
import com.google.gson.Gson
import com.mapbox.mapboxsdk.plugins.localization.BuildConfig
@@ -151,6 +152,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
}
override suspend fun doWork(): Result {
+ var countUpload = 0
notificationManager = NotificationManagerCompat.from(appContext)
val processingUploads = getNotificationBuilder(
CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL
@@ -201,6 +203,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
contribution.transferred = 0
contribution.state = Contribution.STATE_IN_PROGRESS
contributionDao.saveSynchronous(contribution)
+ setProgressAsync(Data.Builder().putInt("progress", countUpload).build())
+ countUpload++
uploadContribution(contribution = contribution)
}
}.collect()
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java
index 0b33ed566..4806585dc 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java
@@ -4,11 +4,12 @@ import android.content.Context;
import android.content.Intent;
public class ActivityUtils {
- public static void startActivityWithFlags(Context context, Class cls, int... flags) {
- Intent intent = new Intent(context, cls);
- for (int flag: flags) {
- intent.addFlags(flag);
+
+ public static void startActivityWithFlags(Context context, Class cls, int... flags) {
+ Intent intent = new Intent(context, cls);
+ for (int flag : flags) {
+ intent.addFlags(flag);
+ }
+ context.startActivity(intent);
}
- context.startActivity(intent);
- }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java
index bb40ee1cf..634a73ad2 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java
@@ -9,22 +9,22 @@ import javax.inject.Singleton;
@Singleton
public class ImageUtilsWrapper {
- @Inject
- public ImageUtilsWrapper() {
+ @Inject
+ public ImageUtilsWrapper() {
- }
+ }
- public Single checkIfImageIsTooDark(String bitmapPath) {
- return Single.fromCallable(() -> ImageUtils.checkIfImageIsTooDark(bitmapPath))
- .subscribeOn(Schedulers.computation());
- }
+ public Single checkIfImageIsTooDark(String bitmapPath) {
+ return Single.fromCallable(() -> ImageUtils.checkIfImageIsTooDark(bitmapPath))
+ .subscribeOn(Schedulers.computation());
+ }
- public Single checkImageGeolocationIsDifferent(String geolocationOfFileString,
- LatLng latLng) {
- return Single.fromCallable(
- () -> ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng))
- .subscribeOn(Schedulers.computation())
- .map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT
- : ImageUtils.IMAGE_OK);
- }
+ public Single checkImageGeolocationIsDifferent(String geolocationOfFileString,
+ LatLng latLng) {
+ return Single.fromCallable(
+ () -> ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng))
+ .subscribeOn(Schedulers.computation())
+ .map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT
+ : ImageUtils.IMAGE_OK);
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java
index 0208c3d57..0ffce9ee4 100644
--- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java
+++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java
@@ -19,43 +19,44 @@ import timber.log.Timber;
@Singleton
public class WikiBaseClient {
- private final WikiBaseInterface wikiBaseInterface;
- private final CsrfTokenClient csrfTokenClient;
+ private final WikiBaseInterface wikiBaseInterface;
+ private final CsrfTokenClient csrfTokenClient;
- @Inject
- public WikiBaseClient(WikiBaseInterface wikiBaseInterface,
- @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) {
- this.wikiBaseInterface = wikiBaseInterface;
- this.csrfTokenClient = csrfTokenClient;
- }
+ @Inject
+ public WikiBaseClient(WikiBaseInterface wikiBaseInterface,
+ @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) {
+ this.wikiBaseInterface = wikiBaseInterface;
+ this.csrfTokenClient = csrfTokenClient;
+ }
- public Observable postEditEntity(String fileEntityId, String data) {
- return csrfToken()
- .switchMap(editToken -> wikiBaseInterface.postEditEntity(fileEntityId, editToken, data)
- .map(response -> (response.getSuccessVal() == 1)));
- }
+ public Observable postEditEntity(String fileEntityId, String data) {
+ return csrfToken()
+ .switchMap(editToken -> wikiBaseInterface.postEditEntity(fileEntityId, editToken, data)
+ .map(response -> (response.getSuccessVal() == 1)));
+ }
- public Observable getFileEntityId(UploadResult uploadResult) {
- return wikiBaseInterface.getFileEntityId(uploadResult.createCanonicalFileName())
- .map(response -> (long) (response.query().pages().get(0).pageId()));
- }
+ public Observable getFileEntityId(UploadResult uploadResult) {
+ return wikiBaseInterface.getFileEntityId(uploadResult.createCanonicalFileName())
+ .map(response -> (long) (response.query().pages().get(0).pageId()));
+ }
- public Observable addLabelstoWikidata(long fileEntityId,
- String languageCode, String captionValue) {
- return csrfToken()
- .switchMap(editToken -> wikiBaseInterface
- .addLabelstoWikidata(PAGE_ID_PREFIX + fileEntityId, editToken, languageCode, captionValue));
+ public Observable addLabelstoWikidata(long fileEntityId,
+ String languageCode, String captionValue) {
+ return csrfToken()
+ .switchMap(editToken -> wikiBaseInterface
+ .addLabelstoWikidata(PAGE_ID_PREFIX + fileEntityId, editToken, languageCode,
+ captionValue));
- }
+ }
- private Observable csrfToken() {
- return Observable.fromCallable(() -> {
- try {
- return csrfTokenClient.getTokenBlocking();
- } catch (Throwable throwable) {
- Timber.e(throwable);
- return "";
- }
- });
- }
+ private Observable csrfToken() {
+ return Observable.fromCallable(() -> {
+ try {
+ return csrfTokenClient.getTokenBlocking();
+ } catch (Throwable throwable) {
+ Timber.e(throwable);
+ return "";
+ }
+ });
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataClient.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataClient.java
index ebee708e9..a476d5b40 100644
--- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataClient.java
+++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataClient.java
@@ -15,32 +15,33 @@ import org.wikipedia.wikidata.Statement_partial;
public class WikidataClient {
- private final WikidataInterface wikidataInterface;
- private final Gson gson;
+ private final WikidataInterface wikidataInterface;
+ private final Gson gson;
- @Inject
- public WikidataClient(WikidataInterface wikidataInterface, final Gson gson) {
- this.wikidataInterface = wikidataInterface;
- this.gson = gson;
- }
+ @Inject
+ public WikidataClient(WikidataInterface wikidataInterface, final Gson gson) {
+ this.wikidataInterface = wikidataInterface;
+ this.gson = gson;
+ }
- /**
- * Create wikidata claim to add P18 value
- *
- * @return revisionID of the edit
- */
- Observable setClaim(Statement_partial claim, String tags) {
- return getCsrfToken()
- .flatMap(csrfToken -> wikidataInterface.postSetClaim(gson.toJson(claim), tags, csrfToken))
- .map(mwPostResponse -> mwPostResponse.getPageinfo().getLastrevid());
- }
+ /**
+ * Create wikidata claim to add P18 value
+ *
+ * @return revisionID of the edit
+ */
+ Observable setClaim(Statement_partial claim, String tags) {
+ return getCsrfToken()
+ .flatMap(
+ csrfToken -> wikidataInterface.postSetClaim(gson.toJson(claim), tags, csrfToken))
+ .map(mwPostResponse -> mwPostResponse.getPageinfo().getLastrevid());
+ }
- /**
- * Get csrf token for wikidata edit
- */
- @NotNull
- private Observable getCsrfToken() {
- return wikidataInterface.getCsrfToken()
- .map(mwQueryResponse -> mwQueryResponse.query().csrfToken());
- }
+ /**
+ * Get csrf token for wikidata edit
+ */
+ @NotNull
+ private Observable getCsrfToken() {
+ return wikidataInterface.getCsrfToken()
+ .map(mwQueryResponse -> mwQueryResponse.query().csrfToken());
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java
index 633dc1818..e283c98a8 100644
--- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java
+++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java
@@ -44,174 +44,172 @@ import timber.log.Timber;
@Singleton
public class WikidataEditService {
- public static final String COMMONS_APP_TAG = "wikimedia-commons-app";
+ public static final String COMMONS_APP_TAG = "wikimedia-commons-app";
- private final Context context;
- private final WikidataEditListener wikidataEditListener;
- private final JsonKvStore directKvStore;
- private final WikiBaseClient wikiBaseClient;
- private final WikidataClient wikidataClient;
- private final Gson gson;
+ private final Context context;
+ private final WikidataEditListener wikidataEditListener;
+ private final JsonKvStore directKvStore;
+ private final WikiBaseClient wikiBaseClient;
+ private final WikidataClient wikidataClient;
+ private final Gson gson;
- @Inject
- public WikidataEditService(final Context context,
- final WikidataEditListener wikidataEditListener,
- @Named("default_preferences") final JsonKvStore directKvStore,
- final WikiBaseClient wikiBaseClient,
- final WikidataClient wikidataClient, final Gson gson) {
- this.context = context;
- this.wikidataEditListener = wikidataEditListener;
- this.directKvStore = directKvStore;
- this.wikiBaseClient = wikiBaseClient;
- this.wikidataClient = wikidataClient;
- this.gson = gson;
- }
-
- /**
- * Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call to
- * the wikibase API to set tag against the entity.
- */
- @SuppressLint("CheckResult")
- private Observable addDepictsProperty(final String fileEntityId,
- final WikidataItem depictedItem) {
-
- final EditClaim data = editClaim(
- ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10)
- : depictedItem.getId()
- );
-
- return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data))
- .doOnNext(success -> {
- if (success) {
- Timber.d("DEPICTS property was set successfully for %s", fileEntityId);
- } else {
- Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
- }
- })
- .doOnError(throwable -> {
- Timber.e(throwable, "Error occurred while setting DEPICTS property");
- ViewUtil.showLongToast(context, throwable.toString());
- })
- .subscribeOn(Schedulers.io());
- }
-
- private EditClaim editClaim(final String entityId) {
- return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName());
- }
-
- /**
- * Show a success toast when the edit is made successfully
- */
- private void showSuccessToast(final String wikiItemName) {
- final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
- final String successMessage = String
- .format(Locale.getDefault(), successStringTemplate, wikiItemName);
- ViewUtil.showLongToast(context, successMessage);
- }
-
- /**
- * Adds label to Wikidata using the fileEntityId and the edit token, obtained from
- * csrfTokenClient
- *
- * @param fileEntityId
- * @return
- */
-
- @SuppressLint("CheckResult")
- private Observable addCaption(final long fileEntityId, final String languageCode,
- final String captionValue) {
- return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
- .doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse))
- .doOnError(throwable -> {
- Timber.e(throwable, "Error occurred while setting Captions");
- ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
- })
- .map(mwPostResponse -> mwPostResponse != null);
- }
-
- private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) {
- if (response != null) {
- Timber.d("Caption successfully set, revision id = %s", response);
- } else {
- Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId);
- }
- }
-
- public Long createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName, final
- Map captions) {
- if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
- Timber
- .d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
- return null;
- }
- return addImageAndMediaLegends(wikidataPlace, fileName, captions);
- }
-
- public Long addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName,
- final Map captions) {
- final Snak_partial p18 = new Snak_partial("value", WikidataProperties.IMAGE.getPropertyName(),
- new ValueString(fileName.replace("File:", "")));
-
- final List snaks = new ArrayList<>();
- for (final Map.Entry entry : captions.entrySet()) {
- snaks.add(new Snak_partial("value",
- WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText(
- new WikiBaseMonolingualTextValue(entry.getValue(), entry.getKey()))));
+ @Inject
+ public WikidataEditService(final Context context,
+ final WikidataEditListener wikidataEditListener,
+ @Named("default_preferences") final JsonKvStore directKvStore,
+ final WikiBaseClient wikiBaseClient,
+ final WikidataClient wikidataClient, final Gson gson) {
+ this.context = context;
+ this.wikidataEditListener = wikidataEditListener;
+ this.directKvStore = directKvStore;
+ this.wikiBaseClient = wikiBaseClient;
+ this.wikidataClient = wikidataClient;
+ this.gson = gson;
}
- final String id = wikidataItem.getId() + "$" + UUID.randomUUID().toString();
- final Statement_partial claim = new Statement_partial(p18, "statement", "normal", id,
- Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks),
- Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName()));
+ /**
+ * Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call
+ * to the wikibase API to set tag against the entity.
+ */
+ @SuppressLint("CheckResult")
+ private Observable addDepictsProperty(final String fileEntityId,
+ final WikidataItem depictedItem) {
- return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle();
- }
-
- public void handleImageClaimResult(final WikidataItem wikidataItem, final Long revisionId) {
- if (revisionId != null) {
- if (wikidataEditListener != null) {
- wikidataEditListener.onSuccessfulWikidataEdit();
- }
- showSuccessToast(wikidataItem.getName());
- } else {
- Timber.d("Unable to make wiki data edit for entity %s", wikidataItem);
- ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
- }
- }
-
- public Observable addDepictionsAndCaptions(final UploadResult uploadResult, final Contribution contribution) {
- return wikiBaseClient.getFileEntityId(uploadResult)
- .doOnError(throwable -> {
- Timber.e(throwable, "Error occurred while getting EntityID to set DEPICTS property");
- ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
- })
- .switchMap(fileEntityId -> {
- if (fileEntityId != null) {
- Timber.d("EntityId for image was received successfully: %s", fileEntityId);
- return Observable.concat(
- depictionEdits(contribution, fileEntityId),
- captionEdits(contribution, fileEntityId)
- );
- } else {
- Timber.d("Error acquiring EntityId for image: %s", uploadResult);
- return Observable.empty();
- }
- }
+ final EditClaim data = editClaim(
+ ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10)
+ : depictedItem.getId()
);
- }
- private Observable captionEdits(Contribution contribution, Long fileEntityId) {
- return Observable.fromIterable(contribution.getMedia().getCaptions().entrySet())
- .concatMap(entry -> addCaption(fileEntityId, entry.getKey(), entry.getValue()));
- }
-
- private Observable depictionEdits(Contribution contribution, Long fileEntityId) {
- final ArrayList depictedItems = new ArrayList<>(contribution.getDepictedItems());
- final WikidataPlace wikidataPlace = contribution.getWikidataPlace();
- if (wikidataPlace != null) {
- depictedItems.add(wikidataPlace);
+ return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data))
+ .doOnNext(success -> {
+ if (success) {
+ Timber.d("DEPICTS property was set successfully for %s", fileEntityId);
+ } else {
+ Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
+ }
+ })
+ .doOnError(throwable -> {
+ Timber.e(throwable, "Error occurred while setting DEPICTS property");
+ ViewUtil.showLongToast(context, throwable.toString());
+ })
+ .subscribeOn(Schedulers.io());
+ }
+
+ private EditClaim editClaim(final String entityId) {
+ return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName());
+ }
+
+ /**
+ * Show a success toast when the edit is made successfully
+ */
+ private void showSuccessToast(final String wikiItemName) {
+ final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
+ final String successMessage = String
+ .format(Locale.getDefault(), successStringTemplate, wikiItemName);
+ ViewUtil.showLongToast(context, successMessage);
+ }
+
+ /**
+ * Adds label to Wikidata using the fileEntityId and the edit token, obtained from
+ * csrfTokenClient
+ *
+ * @param fileEntityId
+ * @return
+ */
+
+ @SuppressLint("CheckResult")
+ private Observable addCaption(final long fileEntityId, final String languageCode,
+ final String captionValue) {
+ return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
+ .doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse))
+ .doOnError(throwable -> {
+ Timber.e(throwable, "Error occurred while setting Captions");
+ ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
+ })
+ .map(mwPostResponse -> mwPostResponse != null);
+ }
+
+ private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) {
+ if (response != null) {
+ Timber.d("Caption successfully set, revision id = %s", response);
+ } else {
+ Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId);
+ }
+ }
+
+ public Long createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName,
+ final Map captions) {
+ if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
+ Timber
+ .d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
+ return null;
+ }
+ return addImageAndMediaLegends(wikidataPlace, fileName, captions);
+ }
+
+ public Long addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName,
+ final Map captions) {
+ final Snak_partial p18 = new Snak_partial("value",
+ WikidataProperties.IMAGE.getPropertyName(),
+ new ValueString(fileName.replace("File:", "")));
+
+ final List snaks = new ArrayList<>();
+ for (final Map.Entry entry : captions.entrySet()) {
+ snaks.add(new Snak_partial("value",
+ WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText(
+ new WikiBaseMonolingualTextValue(entry.getValue(), entry.getKey()))));
+ }
+
+ final String id = wikidataItem.getId() + "$" + UUID.randomUUID().toString();
+ final Statement_partial claim = new Statement_partial(p18, "statement", "normal", id,
+ Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks),
+ Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName()));
+
+ return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle();
+ }
+
+ public void handleImageClaimResult(final WikidataItem wikidataItem, final Long revisionId) {
+ if (revisionId != null) {
+ if (wikidataEditListener != null) {
+ wikidataEditListener.onSuccessfulWikidataEdit();
+ }
+ showSuccessToast(wikidataItem.getName());
+ } else {
+ Timber.d("Unable to make wiki data edit for entity %s", wikidataItem);
+ ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
+ }
+ }
+
+ public Observable addDepictionsAndCaptions(final UploadResult uploadResult,
+ final Contribution contribution) {
+ return wikiBaseClient.getFileEntityId(uploadResult)
+ .doOnError(throwable -> {
+ Timber
+ .e(throwable, "Error occurred while getting EntityID to set DEPICTS property");
+ ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
+ })
+ .switchMap(fileEntityId -> {
+ if (fileEntityId != null) {
+ Timber.d("EntityId for image was received successfully: %s", fileEntityId);
+ return Observable.concat(
+ depictionEdits(contribution, fileEntityId),
+ captionEdits(contribution, fileEntityId)
+ );
+ } else {
+ Timber.d("Error acquiring EntityId for image: %s", uploadResult);
+ return Observable.empty();
+ }
+ }
+ );
+ }
+
+ private Observable captionEdits(Contribution contribution, Long fileEntityId) {
+ return Observable.fromIterable(contribution.getMedia().getCaptions().entrySet())
+ .concatMap(entry -> addCaption(fileEntityId, entry.getKey(), entry.getValue()));
+ }
+
+ private Observable depictionEdits(Contribution contribution, Long fileEntityId) {
+ return Observable.fromIterable(contribution.getDepictedItems())
+ .concatMap(wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem));
}
- return Observable.fromIterable(depictedItems)
- .concatMap(wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem));
- }
}
diff --git a/app/src/main/res/layout/activity_quiz.xml b/app/src/main/res/layout/activity_quiz.xml
index e27fcdc71..ec90a9c99 100644
--- a/app/src/main/res/layout/activity_quiz.xml
+++ b/app/src/main/res/layout/activity_quiz.xml
@@ -57,22 +57,21 @@
android:layout_marginTop="@dimen/activity_margin_vertical"
android:layout_marginBottom="@dimen/activity_margin_vertical"
android:layout_height="wrap_content"
- android:layout_width="wrap_content" />
+ android:layout_width="match_parent" />
-
-
+
diff --git a/app/src/main/res/layout/answer_layout.xml b/app/src/main/res/layout/answer_layout.xml
index d8953f65b..aa112432d 100644
--- a/app/src/main/res/layout/answer_layout.xml
+++ b/app/src/main/res/layout/answer_layout.xml
@@ -1,65 +1,43 @@
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/bottom_container_location_picker.xml b/app/src/main/res/layout/bottom_container_location_picker.xml
index 95c0a2431..2eafaa4b6 100644
--- a/app/src/main/res/layout/bottom_container_location_picker.xml
+++ b/app/src/main/res/layout/bottom_container_location_picker.xml
@@ -1,6 +1,5 @@
-
+ app:srcCompat="@drawable/ic_check_black_24dp" />
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/caption_item.xml b/app/src/main/res/layout/caption_item.xml
new file mode 100644
index 000000000..0e78d75cd
--- /dev/null
+++ b/app/src/main/res/layout/caption_item.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_media_detail.xml b/app/src/main/res/layout/fragment_media_detail.xml
index ef691c300..cfbb01094 100644
--- a/app/src/main/res/layout/fragment_media_detail.xml
+++ b/app/src/main/res/layout/fragment_media_detail.xml
@@ -23,6 +23,20 @@
layout="@layout/layout_edit_categories" />
+
+
+
+
+
@@ -187,9 +202,19 @@
android:padding="@dimen/small_gap"
android:textColor="?attr/mediaDetailsText"
android:textSize="@dimen/description_text_size"
+ android:textIsSelectable="true"
tools:text="Description of the media goes here. This can potentially be fairly long, and will need to wrap across multiple lines. We hope it looks nice though." />
+
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:background="?attr/mainBackground"
+ android:elevation="30dp">
-
+
+
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/standard_gap"
+ >
+
+
+
+
+
+
+
+
+
+
-
-
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ >
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/show_captions_descriptions.xml b/app/src/main/res/layout/show_captions_descriptions.xml
new file mode 100644
index 000000000..26d1fbc7a
--- /dev/null
+++ b/app/src/main/res/layout/show_captions_descriptions.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 88c2e30d5..029b5c95a 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -5,6 +5,7 @@
* Azouz.anis
* ButterflyOfFire
* Claw eg
+* Dr. Mohammed
* Kassem7899
* Meno25
* Mido
@@ -506,6 +507,7 @@
لأنها
تحديث التصنيف
تحديث التصنيفات
+ نجاح
مشاركة الصور عبر
أنت لم تقدم أية مساهمات حتى الآن
تم إنشاء الحساب!
@@ -544,4 +546,8 @@
تأكيد
التعليمات
اتصال محدود
+ اختر موقعًا
+ حدد موقعًا
+ أضف الموقع
+ مكان الصورة
diff --git a/app/src/main/res/values-ban/error.xml b/app/src/main/res/values-ban/error.xml
new file mode 100644
index 000000000..4933d36d0
--- /dev/null
+++ b/app/src/main/res/values-ban/error.xml
@@ -0,0 +1,10 @@
+
+
+
+ Commons kantun usak
+ Mimih. Wénten sané iwang!
+ Bangyang iraga nawang napi sané ragané margiang, lantas wedar saking rerepél majeng iraga. Pacang ngawantu iraga ngabecikin!
+ Matur suksma!
+
diff --git a/app/src/main/res/values-ban/strings.xml b/app/src/main/res/values-ban/strings.xml
index 4cde5a165..f8e53c1f0 100644
--- a/app/src/main/res/values-ban/strings.xml
+++ b/app/src/main/res/values-ban/strings.xml
@@ -87,6 +87,8 @@
Lis
(Durung kaunggah)
Nénten wénten kategori sané patut sareng %1$s
+ %1$s ten ngelah kelas turunan
+ %1$s ten ngelah kelas rerama
Kategori
Setélan
Daptar
@@ -94,21 +96,37 @@
Kategori
Ulasan Peer
Indik
+ Kardi <a href=\"%1$s\">isu GitHub</a> anyar antuk parihindik miwah panikayan kakutu.
Parikrama paragaan
Krédit
Indik
+ Kirim umpan walik (liwat Rerepél)
+ Kategori sané mangkin kaanggén
+ Ngantosang sinkronisasi kapertama…
Indayang malih
Wangdé
Unduh
Lisénsi baku
Anggén murda miwah pidarta sadurungné
Téma
+ Atribusi 4.0
+ Atribusi 3.0
CC0
CC BY-SA 3.0
CC BY 3.0
CC BY-SA 4.0
CC BY 4.0
+ Objék palemahan (sekar, baburon, gunung)
+ Objék mawiguna (sepeda, stasiun sepur)
+ Jadma kasub (bupati ragané, atlét Olimpiade sané ragané tepukin)
+ Durus SAMPUNANG ngunggah:
+ Potrék sélfi utawi potrék timpal ragané
+ Gambar sané kaunduh saking internét
+ Tangkepan layar satunggil aplikasi
Conto unggahan:
+ Murda: Wangunan Opera Sydney
+ Pidarta: Wangunan Opera Sydney kacingak saking sebrang celuk
+ Kategori: Wangunan Opera Sydney saking kauh, pacingakan Wangunan Opera Sydney
Napiké Ida ngartos?
Inggih!
Pidarta lianan
@@ -116,6 +134,10 @@
Ngamuat…
Nénten wénten kapilih
Tanpa sasirah
+ Tanpa pidarta
+ Tanpa pabligbagan
+ Lisénsi nénten kauningin
+ Segerang
OK
Paingetan
Katemuin Gambar Kaduplikat
@@ -125,6 +147,9 @@
Sesirah
Murda
Pabligbagan
+ Tanggal kaunggah
+ Lisénsi
+ Koordinat
Kode O2F
Logo Commons
Situs Commons
@@ -147,13 +172,38 @@
Setélan
Umpan walik
Medal log
+ Panuntun
+ Turéksa
+ Kaca berkas Commons
Suratan Wikipédia
+ Gambar bes peteng.
+ Gambar burem.
+ Gambar sampun wénten ring Commons
+ Gamble kaambil ring genah tiosan.
+ Durus wantah unggaj gambar sané ragané ambil ngaraga. Sampunang unggah gambar sané ragané temuin ring akun Facebook jadma liyanan.
+ Napiké ragané kantunjagi ngunggah gambar puniki?
+ Galat sambungan
+ Prosés pangunggahan perlu aksés internét urip. Durus turéksa sambungan jaringan ragané.
+ Pikobet katemuin ring gambar
+ Manjing log nuju akun ragané
Kirim berkas log
Liwatin
Manjing log
+ Paarah
Wikidata
Wikipédia
+ Commons
+ Rating iraga
+ Liwatin Tutorial
+ Pangalih basa
Basa
+ Kamargiang
+ Wangdé
+ Indayang malih
+ Gambar ten katemu!
+ Kaunggah olih: %1$s
+ Gambar rahina mangkin
+ Gambar rahina mangkin
Rereh
Rereh Commons
Rereh
@@ -161,16 +211,66 @@
Kuéri parerehan sané mangkin
Média
Kategori
+ Kaunggah saking sélulér
+ Dadosang wallpaper
+ Wallpaper sampun kapasang!
+ Napiké gambar puniki OK antuk kaunggah?
+ Pitakén
+ Asil
Lanturang
Cawisan sané becik
+ Pisaur Iwang
+ Napiké Ida jagi ngaresikin lelintihan parerehan Ida?
+ Napiké ragané jagi ngusap parerehan puniki?
+ Lelintihan parerehan kausap
+ Usulan Pangusapan
Usap
+ Panghargaan
+ Profil
Statistik
Haturan Suksma Katampi
+ Gambar Pilihan
Tingkat
+ Gambar Kaupload
+ Gambar Kaanggén
+ Pituut
Nampek
Lis
+ Langkat %1$d saking %2$d: %3$s
+ Salanturné
+ Sadurungné
+ Kumpulang
Gambar
Genah
Puput
+ Kirim Suksma majeng %1$s
+ Gambar salanturnyané
+ Nggih, ngujang ten
+ Cingak wacén
+ Cingak durung kawacén
+ Jantos dumun…
+ Katurun
+ Piranti lunak
+ Pangunggahan Kawangdé
+ %1$s kaunggah olih: %2$s
Logo
+ MÉDIA
+ Pangaturan
+ Muat luwih akéh
+ Patunjuk
+ Senengan
+ Ngawarsa
+ Ngawuku
+ Makejang kala
+ Unggah
+ Nampek
+ Kaanggén
+ Paringkat Titiang
+ Télémétri Mapbox
+ Kualitas Gambar
+ Ngalanturang unggahan...
+ Lisénsi Média
+ Ring samian basa
+ Pilihin genah
+ Pilih Genah
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 4f9b0bda2..8b42615f6 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -236,7 +236,10 @@
Това място вече не съществува.
Не са открити изображения!
Търсене
+ Търсене в Общомедия
Търсене
+ Скорошни търсения:
+ Скорошни заявки за търсене
Грешка при зареждането на категориите.
Грешка при зареждането на описанията.
Мултимедия
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
index d6b819434..f9de7a473 100644
--- a/app/src/main/res/values-bn/strings.xml
+++ b/app/src/main/res/values-bn/strings.xml
@@ -312,6 +312,7 @@
সফল
বিষয়শ্রেণী হালনাগাদ করা সম্ভব হয়নি।
বিষয়শ্রেণীগুলি হালনাগাদ করুন
+ সফল
অ্যাকাউন্ট তৈরি করা হয়েছে!
বুকমার্ক থেকে সরানো হয়েছে
বুকমার্কে যোগ করা হয়েছে
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 6ca0bbabf..286907bc5 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -322,6 +322,7 @@
Succes
Kunne ikke tilføje kategorier.
Opdater kategorier
+ Succes
Del billede via
Konto oprettet!
Der var en fejl!
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 1007bfad3..2cb981aeb 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -116,7 +116,7 @@
Liste
(Noch keine hochgeladenen Dateien)
Die Kategorie „%1$s“ wurde nicht gefunden
- Keine Wikidata Gegenstände gefunden für %1$s
+ Kein Wikidata-Objekt für %1$s gefunden
%1$s hat keine child-Klasse
%1$s hat keine parent-Klasse
Füge Kategorien hinzu, um deine Bilder auf Wikimedia Commons auffindbarer zu machen.\nBeginne mit der Eingabe, um Kategorien hinzuzufügen.
@@ -306,7 +306,7 @@
Beim Laden von Darstellungen ist ein Fehler aufgetreten.
Medien
Kategorien
- Einträge
+ Objekte
Vorgestellt
Über mobil hochgeladen
Bild zu %1$s auf Wikidata hinzugefügt!
@@ -512,10 +512,15 @@
Text in die Zwischenablage kopiert
Benachrichtigung als gelesen markieren
Ein Fehler ist aufgetreten!
+ Zustand des Platzes:
Existiert
Benötigt Foto
+ Platztyp:
Brücke, Museum, Hotel etc.
Bei der Anmeldung ist etwas schief gelaufen, du musst dein Passwort zurücksetzen!
+ MEDIA
+ CHILD CLASSES
+ PARENT CLASSES
Ort in der Nähe gefunden
Ist dies ein Foto vom Ort %1$s?
Lesezeichen
@@ -525,6 +530,7 @@
Etwas ist schief gelaufen. Konnte das Hintergrundbild nicht einstellen
Als Hintergrundbild festlegen
Hintergrundbild wird festgelegt. Bitte warten...
+ System folgen
Dunkel
Hell
Die Standorteinstellungen konnten nicht geöffnet werden. Bitte schalte den Standort manuell ein
@@ -546,6 +552,7 @@
3. Suche einen geeigneten Abschnitt im Artikel für dein Bild
4. Klicken Sie auf das Symbol „Bearbeiten“ (das einem Stift ähnelt) für diesen Abschnitt.
5. Füge den wikitext an der entsprechenden Stelle ein.
+ 6. Bearbeite den Wikitext für eine geeignete Positionierung, falls erforderlich. Weitere Informationen findest du <a href=\"https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images#How_to_place_an_image\">hier</a>.
7. Veröffentliche den Artikel
wikicode in die Zwischenablage kopieren
pausieren
@@ -592,11 +599,19 @@
Medienlizenz
Mediendetails
Kategorieseite anzeigen
+ Objektseite ansehen
Benutzeroberflächensprache
+ Entfernt eine Beschriftung und Beschreibung
Mehr lesen
+ In allen Sprachen
Wähle einen Ort
Schwenken und Zoomen zum Einstellen
Beenden der Ortswahl
Ort auswählen
- Ortauswahl Bildansicht
+ In Karten-App anzeigen
+ Standort beabeiten
+ Die Bildansicht der Standortauswahl
+ Der Schatten der Bildansicht der Ortsauswahl
+ Bildstandort
+ Überprüfe, ob der Standort korrekt ist
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index e78386941..a8ace28ec 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -4,6 +4,7 @@
* Domdomegg
* Evropi
* Geraki
+* Giannaras99
* Giorgos456
* Glavkos
* KATRINE1992
@@ -359,4 +360,5 @@
Ανέβασμα
Γειτονικά
Σε χρήση
+ Για όλες τις γλώσσες
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index ef3d9be0f..2b06ead76 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -2,6 +2,7 @@
@@ -62,4 +63,6 @@
Categoriae
Depromens…
Nulla selecta
+ Mutare locus
+ Locus imaginis
diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml
index 6d7feec72..f68fe56a1 100644
--- a/app/src/main/res/values-lb/strings.xml
+++ b/app/src/main/res/values-lb/strings.xml
@@ -301,6 +301,8 @@
Succès
Kategorie konnten net derbäigesat ginn.
Kategorien aktualiséieren
+ Aktualiséierung vun de Koordinaten
+ Succès
Benotzerkont ugeluecht!
Brauch eng Foto
Plaz nobäi fonnt
@@ -349,4 +351,7 @@
Limitéierte Verbindungsmodus
Sprooch vum Interface vum Benotzer vun der App
Liest méi
+ An alle Sproochen
+ Eng Plaz eraussichen
+ Plaz eraussichen
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index 8fcf168f5..9f783d968 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -584,10 +584,15 @@
Јазик на прилогот
Острани толкување и опис
Прочитајте повеќе
+ На сите јазици
Изберете местоположба
Доведете го местото на картата и приближете го за да прилагодите
Излези од избирачот на местоположба
Изберете местоположба
- Поглед на слика за избирање местоположба
- Сенчест поглед на слика за избирање местоположба
+ Прикажи во прилог за карти
+ Уреди местоположба
+ Сликовен поглед на избирачот на местоположба
+ Сенка на сликата за избирање местоположба
+ Местоположба на сликата
+ Проверете дали местоположбата е точна
diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml
index 9c87b6356..6240fb0d8 100644
--- a/app/src/main/res/values-pms/strings.xml
+++ b/app/src/main/res/values-pms/strings.xml
@@ -582,10 +582,15 @@
Lenga d\'antërfassa utent ëd l\'aplicassion
A gava na legenda e na descrission
Lese ëd pi
+ An tute le lenghe
Serne na locassion
Slarghé e strenze për buté bin
Seurte dal selessionador ëd locassion
Selessioné na locassion
- Vista dla plancia dël selessionador ëd locassion
- Locassion ëd picker_image_view_shadow
+ Smon-e andrinta a l\'aplicassion carta
+ Modifiché ël pòst
+ La vista dla plancia dël selessionador ëd locassion
+ L\'ombra dla vista dla plancia dël selessionator ëd pòst
+ Pòst ëd la plancia
+ Controlé si ël pòst a l\'é giust
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 030604703..6101bab17 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -495,6 +495,12 @@
Não foi possível adicionar categorias.
Atualizar categorias
+ Tentando atualizar as coordenadas.
+ Atualização de coordenadas
+ Sucesso
+ Coordenadas %1$s adicionadas.
+ Não foi possível adicionar coordenadas.
+ Incapaz de obter coordenadas.
Compartilhar imagem via
Você ainda não fez nenhuma contribuição
Conta criada!
@@ -592,4 +598,11 @@
Idioma da interface do usuário do aplicativo
Remove uma legenda e descrição
Leia mais
+ Em todas os idiomas
+ Escolha uma localização
+ Selecionar localização
+ Mostrar no aplicativo de mapa
+ Editar localização
+ Localização da imagem
+ Verifique se a localização está correta
diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml
index e234bd25a..3d065ac3b 100644
--- a/app/src/main/res/values-qq/strings.xml
+++ b/app/src/main/res/values-qq/strings.xml
@@ -170,4 +170,5 @@
{{Identical|More}}
{{Identical|Favorite}}
<code>&#169;</code> is the copyright symbol (©).
+ A description of a visual element, location picker image shadow. Used for accesibility usually.
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 7b0740c11..618d3f3cc 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -525,6 +525,12 @@
Не удалось добавить категории.
Обновить категории
+ Попытка обновить координаты.
+ Обновление координат
+ Успешно
+ Координаты %1$s добавлены.
+ Не удалось добавить координаты.
+ Не удалось получить координаты.
Поделиться изображением с помощью
Пока что от вас нет вклада
Учётная запись создана!
@@ -622,4 +628,11 @@
Язык пользовательского интерфейса приложения
Удаляет подпись и описание
Подробнее
+ На всех языках
+ Выберите местоположение
+ Панорамируйте и масштабируйте для настройки
+ Выйти из окна выбора местоположения
+ Выберите местоположение
+ Показать в приложении карты
+ Проверьте правильность местоположения
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index ffe648b2b..61f5b8def 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -502,6 +502,12 @@
Nepodarilo sa pridať kategórie.
Aktualizovať kategórie
+ Pokus o aktualizáciu súradníc.
+ Aktualizácia súradníc
+ Úspech
+ Súradnice %1$s sú pridané.
+ Nepodarilo sa pridať súradnice.
+ Nepodarilo sa získať súradnice.
Zdieľať obrázok cez
Nemáte zatiaľ žiadne príspevky
Účet bol vytvorený!
@@ -599,4 +605,15 @@
Jazyk používateľského rozhrania aplikácie
Odstráni titulky a popis
Čítať viac
+ Pre všetky jazyky
+ Vybrať polohu
+ Posúvať a zväčšovať pre nastavenie
+ Odísť z výberu polohy
+ Vybrať lokalizáciu
+ Zobraziť na mape v aplikácii
+ Upraviť polohu
+ Obrázok výberu polohy
+ Tieň obrázka pre výber polohy
+ Poloha obrázka
+ Skontrolovať či je poloha správna
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 5864d2626..c3be6ee2f 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -486,6 +486,12 @@
Kunde inte lägga till kategorier.
Uppdatera kategorier
+ Försöker uppdatera koordinater.
+ Koordinater uppdaterades
+ Genomfördes
+ Koordinaterna %1$s lades till.
+ Kunde inte lägga till koordinater.
+ Kunde inte hämta koordinater.
Dela bild via
Du har ännu inte gjort några bidrag
Konto har skapats!
@@ -583,4 +589,15 @@
Appens användargränssnittspråk
Ta bort en bildtext och beskrivning
Läs mer
+ På alla språk
+ Välj en plats
+ Panorera och zooma för att justera
+ Avsluta platsväljaren
+ Välj plats
+ Visa i kartappen
+ Redigera plats
+ Platsväljarens bildvisare
+ Skuggan för platsväljarens bildvisare
+ Bildplats
+ Kontrollera om platsen är korrekt
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 05c535953..e148e5dfd 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -501,6 +501,12 @@
Kategoriler eklenemedi.
Kategorileri güncelle
+ Koordinatlar güncellenmeye çalışılıyor.
+ Koordinat güncellemesi
+ Başarılı
+ %1$s koordinatları eklendi.
+ Koordinatlar eklenemedi.
+ Koordinatlar alınamadı.
Resmi şununla paylaş
Henüz bir katkı yapmadınız
Hesap oluşturuldu!
@@ -598,4 +604,15 @@
Uygulama kullanıcı arayüzü dili
Bir başlığı ve açıklamayı kaldırır
Devamını oku
+ Tüm dillerde
+ Bir konum seçin
+ Ayarlamak için kaydırma ve yakınlaştırma
+ Konum seçiciden çık
+ Konum seçin
+ Harita uygulamasında göster
+ Konumu düzenle
+ Konum seçicinin resim görünümü
+ Konum seçicinin resim görünümünün gölgesi
+ Görüntü Konumu
+ Konumun doğru olup olmadığını kontrol edin
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 0dd3cd985..01676ca8d 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -4,6 +4,7 @@
* Alexander Yukal
* Andriykopanytsia
* Base
+* DDPAT
* Movses
* Mykola Swarnyk
* Piramidion
@@ -148,7 +149,7 @@
Усталена ліцензія
Використати попередні назву й опис
Тема
- Attribution-ShareAlike 4.0
+ Attribution-ShareAlike 4.0
Attribution 4.0
Attribution-ShareAlike 3.0
CC Attribution 3.0
@@ -193,7 +194,7 @@
Гаразд
Попередження
Виявлено дублікат зображення
- Вивантажити
+ Завантажити
Так
Ні
Підпис
@@ -207,7 +208,7 @@
Координати
Не передбачено
Станьте бета-тестером
- Підпишіться на наш бета-канал на Google Play і отримайте ранній доступ до нових функцій та виправлень багів
+ Підпишіться на наш бета-канал на Google Play і отримайте ранній доступ до нових функцій та виправлень баґів
Код 2FA
Ви справді хочете вийти із системи?
Логотип Вікісховища
@@ -234,7 +235,7 @@
Налаштування
Зворотний зв\'язок
Вийти
- Посібник
+ Керівництво
Сповіщення
Перевірка
опис не знайдено
@@ -250,6 +251,8 @@
Зображення виконано в іншому місці.
Будь ласка, завантажуйте тільки ті зображення, які були зроблені вами. Не завантажуйте зображень, які ви знайшли у Фейсбуці.
Ви все одно хочете завантажити це зображення?
+ Помилка з\'єднання
+ Процес вивантаження вимагає активного інтернет-з\'єднання. Будь ласка, перевірте своє підключення до мережі.
Виявлено проблеми із зображенням
Будь ласка, завантажуйте тільки ті зображення, які були зроблені вами. Не завантажуйте зображень, які ви знайшли в інтернеті.
Зберігати зроблені в програмці знімки
@@ -267,8 +270,8 @@
Ви дійсно бажаєте пропустити автентифікацію?
Вам треба буде увійти в систему для завантаження зображень у майбутньому.
Увійдіть, щоб використати цю функцію
- Скопіювати вікітекст у буфер обміну
- Вікітекст скопійовано у буфер обміну
+ Скопіювати вікі-текст у буфер обміну
+ Вікі-текст скопійовано у буфер обміну
Функція «Поблизу» може працювати некоректно, «Розташування» недоступне.
Потрібний дозвіл для показу списку місць поблизу
Напрямки
@@ -319,10 +322,10 @@
Чи можна завантажувати це зображення?
Запитання
Результат
- Якщо ви продовжите завантажувати зображення, які підлягають вилученню, ваш обліковий запис можуть забанити. Ви впевнені, що хочете закінчити тест?
- Понад %1$s завантажених вами зображень було вилучено. Якщо ви продовжите завантажувати зображення, які підлягають вилучення, ваш обліковий запис можуть забанити. \n\nХочете переглянути посібник ще раз і тоді пройти тест, що допоможе дізнатися, які зображення можна, а які не можна завантажувати?
- Селфі не мають значної енциклопедичної цінності. Будь ласка, не завантажуйте фото самих себе, якщо про вас нема статті у Вікіпедії.
- Зображення пам\'яток і пейзажів можна завантажувати у більшості країн. Зважте, будь ласка, що сучасні мистецькі інсталяції на вулиці часто захищені авторськими правами і їх НЕ можна завантажувати.
+ Якщо ви продовжите завантажувати зображення, які підлягають вилученню, ваш обліковий запис можуть заблокувати. Ви впевнені, що хочете закінчити тест?
+ Понад %1$s завантажених вами зображень було вилучено. Якщо ви продовжите завантажувати зображення, які підлягають вилучення, ваш обліковий запис можуть заблокувати. \n\nХочете переглянути посібник ще раз і тоді пройти тест, що допоможе дізнатися, які зображення можна, а які не можна завантажувати?
+ Селфі не мають значної енциклопедичної цінності. Будь ласка, не завантажуйте фото самих себе, якщо про вас немає статті у Вікіпедії.
+ Зображення пам\'яток і пейзажів можна завантажувати у більшості країн. Зауважте, будь ласка, що сучасні мистецькі інсталяції на вулиці часто захищені авторськими правами і їх НЕ можна завантажувати.
Знімки екрану веб-сайтів вважаються похідними роботами і є таким же об\'єктом авторських прав, як і сам веб-сайт. Їх можна використовувати з дозволу авторів сайту. Без такого дозволу, будь-який твір, який ви створите на основі їхньої роботи, юридично вважається неліцензованою копією, яка належить оригінальному автору.
Одна з цілей Вікісховища — зібрати якісні зображення. Тому розмиті зображення завантажувати не треба. Завжди старайтеся зробити гарні знімки при хорошому освітленні.
Зображення технологій чи культури дуже бажані для Вікісховища.
@@ -333,7 +336,7 @@
Продовжити
Правильна відповідь
Хибна відповідь
- Чи можна завантажувати цей скріншот?
+ Чи можна завантажувати цей знімок?
Поширити програму
Помилка отримання місць поблизу.
Історія пошуку порожня
@@ -358,7 +361,7 @@
Кількість зображень, які ви завантажили у Вікісховище будь-яким методом
Відсоток завантажених вами у Вікісховище зображень, що не були вилучені
Кількість завантажених вами у Вікісховище зображень, що використані у статтях Вікімедіа
- Відбулася помилка!
+ Сталася помилка!
Сповіщення Вікісховища
Використати альтернативне ім\'я автора
При завантаженні фото використовувати альтернативне ім\'я для зазначення авторства замість власного імені користувача
@@ -409,7 +412,7 @@
Ви зробили так багато, що наша система підрахунку досягнень не може впоратись зі своїм завданням. Це — абсолютне досягнення.
Завершується:
Показати кампанії
- Чинні кампанії
+ Чинні компанії
Ви більше не бачитимете кампаній. Однак Ви можете увімкнути це сповіщення повторно в своїх налаштуваннях, якщо забажаєте.
Ця функція вимагає доступу до інтернету. Будь ласка, перевірте своє з\'єднання.
Сталася помилка при обробці зображення. Будь ласка, спробуйте ще раз!
@@ -454,7 +457,7 @@
Сталася помилка при завантаженні зображень
Будь ласка, зачекайте…
Вибрані зображення — це зображення від вправних фотографів та ілюстраторів, які спільнота Вікісховища визначила як такі, що мають найкращу якість на сайті.
- Зображення, завантажені через «Поблизу», — це зображення, завантажені через дослідження місць на карті.
+ Зображення, завантажені через «Поблизу», — це зображення, завантажені через дослідження місць на мапі.
Ця функція дозволяє редакторам надіслати «дякую» користувачам, які роблять корисні редагування, — скориставшись невеличким посиланням на сторінці історії або порівняння версій.
Копіювати до наступних медіафайлів
Скопійовано
@@ -504,8 +507,14 @@
Неможливо додати категорії
Оновити категорії
+ Спроба оновити координати.
+ Оновлення координат
+ Успіх
+ Координати %1$s додані.
+ Не вдалося додати координати.
+ Не вдалося отримати координати.
Поширити зображення через
- Поки що від вас немає вкладу
+ Ви ще не зробили жодного внеску
Обліковий запис створено!
Текст скопійовано до буферу обміну
Сповіщення позначено прочитаним
@@ -545,12 +554,12 @@
Ви хочете додати це зображення у статтю Вікіпедії мовою %1$s?
Підтвердити
Інструкції
- 1. Використовуйте такий вікітекст:
+ 1. Використовуйте такий вікі-текст:
2. Натискання на «Підтвердити» відкриє статтю Вікіпедії
3. Знайдіть розділ статті, до якого пасуватиме ваше зображення
4. Натисніть на іконку «Редагувати» (у вигляді олівця) біля цього розділу.
- 5. Вставте вікітекст у підхожому місці.
- 6. Відредагуйте вікітекст за потреби, вказавши потрібне розміщення. Детальнішу інформацію знайдете <a href=\"https://uk.wikipedia.org/wiki/Довідка:Розширений_синтаксис_зображень\">тут</a>.
+ 5. Вставте вікі-текст у відповідне місце.
+ 6. Відредагуйте вікі-текст за потреби, вказавши потрібне розміщення. Детальну інформацію знайдете <a href=\"https://uk.wikipedia.org/wiki/Довідка:Розширений_синтаксис_зображень\">тут</a>.
7. Опублікуйте статтю
Скопіювати вікікод у буфер обміну
пауза
@@ -579,16 +588,16 @@
Мій ранг
Телеметрія Mapbox
Надсилати анонімізоване розташування та дані щодо користування до Mapbox при використанні функції Поблизу
- © <a href=\"https://www.mapbox.com/about/maps/\">Mapbox</a> © <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> <a href=\"https://www.mapbox.com/map-feedback/\">Покращити цю карту</a>
+ © <a href=\"https://www.mapbox.com/about/maps/\">Mapbox</a> © <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> <a href=\"https://www.mapbox.com/map-feedback/\">Покращити цю мапу</a>
Увімкнено режим обмеженого з\'єднання!
Вимкнено режим обмеженого з\'єднання. Завантаження в процесі очікування тепер відновляться.
Режим обмеженого з\'єднання
Якісні зображення
- Якісні зображення — це діаграми чи фотографії, що відповідають певним стандартам якості (переважно технічним за природою) та цінні для проектів Вікімедіа
+ Якісні зображення — це діаграми чи фотографії, що відповідають певним стандартам якості (переважно технічним за природою) та цінні для проєктів Вікімедіа
Продовження завантаження…
Призупинення завантаження…
- Ви ввімкнули режим обмеженого з\'єднання. Усі завантаження призупинено та буде відновлено коли Ви вимкнете цей режим.
- Режим обмеженого з\'єднання ввімкнено.
+ Ви увімкнули режим обмеженого з\'єднання. Усі завантаження призупинено та буде відновлено коли Ви вимкнете цей режим.
+ Режим обмеженого з\'єднання увімкнено.
Будь ласка, напишіть короткий опис, що розповідає що представлено на зображенні. В описі розкажіть що робить це зображення цікавим, типовим або рідкісним, опишіть контекст — видимий чи ні. Старайтесь максимально використовувати точну термінологію.
Будь ласка, знайдіть та оберіть всі концепти, що це зображення показує. Будьте настільки точними, наскільки можливо. Якщо зображення зображує декілька речей, у межах здорового глузду оберіть їх всі. Не обирайте загальніші теґи, коли є доступні точніші.
Будь ласка, оберіть відповідні категорії. На відміну від описів, назви категорій лише англійською.
@@ -601,4 +610,15 @@
Мова інтерфейсу
Вилучає опис і підпис
Читати більше
+ Усіма мовами
+ Вибрати розташування
+ Панорамуйте і збільшуйте, щоб підлаштувати
+ Вийти з вікна вибору розташування
+ Вибрати розташування
+ Показати в додатку на мапі
+ Редагувати підпис
+ Вигляд вибору розташування як зображення
+ Вигляд вибору розташування як тіні зображення
+ Підпис зображення
+ Перевірте правильність розташування
diff --git a/app/src/main/res/values-xmf/strings.xml b/app/src/main/res/values-xmf/strings.xml
index 5abd96563..4f3d8bbd9 100644
--- a/app/src/main/res/values-xmf/strings.xml
+++ b/app/src/main/res/values-xmf/strings.xml
@@ -32,7 +32,7 @@
მიოჯინი
თარი
წიმოხრსხუ
- კონფიდენციალურობა
+ კონფიდენციალურალა
ვიკიოწკარუე
პარამეტრეფი
ვიკიოწკარუეშა ეხარგუა
@@ -69,6 +69,7 @@
გობჟინაფა
ფაილიშ ხასჷლაშ ძირაფა
მუკნაჭარა (უციო)
+ ქორთხინთ, ქემიოწურეთ თე ფაილიშ ეჭარუა
ეჭარუა
მუკნაჭარა
მიშულაქ ვემიხუჯინუ - რშვილიშ ჩილათა
@@ -98,7 +99,7 @@
<a href=\"https://github.com/commons-app/apps-android-commons\">წყუ</a> და <a href=\"https://commons-app.gოthub.io/\">ვებ-ხასჷლა</a> GitHub-ის. ჩილათაშ ოგინაფალო ვარ-და ზიტყვასქვილშო გაჭყით ახალი <a href=\"%1$s\">მოთხირი GitHub-ის</a>.
კონფიდენციალურალაშ პოლიტიკა
<a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">მარდეფი</a>
- პრგრამაშ გეშა
+ პროგრამაშ გეშა
წჷმიხონარეფიშ ჯღონუა (ელ.ფოსტათ)
ელ-ფოსტაშ კლიენტი ვა რე გერინაფილი
ასერდე გჷმორინაფილი კატეგორიეფი
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 894c9647e..3aa28b414 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -239,6 +239,8 @@
此圖片是在不同的地點拍攝。
請僅上傳您自己拍攝的圖片。不要上傳您從別人臉書帳號裡所找到的圖片。
您仍要上傳此圖片嗎?
+ 連接錯誤
+ 更新處理需要有效的網際網路存取。請檢查您的網路連線。
在圖片裡發現問題
請僅上傳您自己拍攝的圖片。不要上傳您從網路下載來的圖片。
儲存應用程式所提供的快照
@@ -491,6 +493,12 @@
無法添加分類。
更新分類
+ 嘗試更新座標。
+ 座標更新
+ 成功
+ 座標 %1$s 已添加。
+ 無法添加座標。
+ 無法取得座標。
分享圖片透過
您還沒有做出任何貢獻
已建立帳號!
@@ -588,4 +596,15 @@
應用程式使用者介面語言
移除說明和描述
延伸閱讀
+ 在所有語言
+ 挑選一個位置
+ 平移和縮放來進行調整
+ 離開位置點選器
+ 選擇位置
+ 顯示在地圖應用程式
+ 編輯位置
+ 位置點選器的圖片檢視
+ 位置點選器的圖片檢視陰影
+ 圖片位置
+ 檢查位置是否正確
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 80155ed23..e55cc2656 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -8,6 +8,7 @@
* Angrydog001
* D41D8CD98F
* Deathkon
+* GuoPC
* Hydra
* Ken418
* Kuailong
@@ -44,13 +45,13 @@
- %1$d次上传
开始上传
-
- - 开始%1$d次上传
- - 开始%1$d次上传
+
+ - 正在处理%d个上传
+ - 正在处理%d个上传
-
- - %1$d次上传
- - %1$d次上传
+
+ - %d个上传
+ - %d个上传
- 该图像的授权协议是 %1$s
@@ -120,6 +121,7 @@
修改
上传
搜索分类
+ 搜索您的媒体描述的项目(如山、泰姬陵等)
保存
刷新
列表
@@ -203,6 +205,7 @@
否
说明
标题
+ 描述
描述
讨论
作者
@@ -246,6 +249,7 @@
维基数据项
维基百科条目
请尽可能详细地描述媒体:拍摄在何地?显示什么?例文是什么?请描述对象或个人。透露一些不易猜想到的信息,例如这幅风景画的具体日期时间。如果媒体显示了一些不寻常的事物,请说明为什么它显得不寻常。
+ 请编写图像的简要说明。第一个标题将作为图片的题目。不能超过255个字符。
这个图片可能的问题:
图片太暗。
图片模糊。
@@ -253,9 +257,11 @@
这张图片是在不同的地点拍摄的。
请仅上传由您自己拍摄的图像。请勿上传您在他人的Facebook账户上所发现的图像。
仍然上传这张图片?
+ 连接错误
+ 上传过程需要有效的互联网访问。请检查您的网络连接。
在图片中发现的问题
请仅上传由您自己拍摄的图像。请勿上传您从互联网下载的图像。
- 使用外部存储
+ 保存应用程序内截图
将您设备内部照相机应用拍摄的照片保存至您的设备存储中
登录您的账户
发送日志文件
@@ -272,7 +278,7 @@
请登录后使用这个功能
复制维基文本到剪贴板
维基文本已经复制到剪贴板
- 位置不可用。
+ 附近可能无法正常工作,位置不可用。
需要权限以显示附近地点列表
方向
维基数据
@@ -308,6 +314,7 @@
最近搜索:
最近的搜索查询
载入分类时发生错误。
+ 加载描述时出错。
媒体
分类
项目
@@ -370,7 +377,7 @@
通知
通知(已读)
显示附近通知
- 点击此处查看最近需要图片的地方
+ 显示有关需要图片的最近地点的应用程序内通知
列表
存储权限
我们需要您的许可才能访问设备的外部存储空间以上传图片。
@@ -398,6 +405,7 @@
未提交分类
没有类别的图像很少可用。确实要继续而不选择类别吗?
没有选择描写
+ 带有描述的图像更容易被发现并且更可能被使用。您确定不选择描述继续吗?
(对于设置中的所有图像)
搜索这个区域
需要许可
@@ -457,6 +465,8 @@
特征图片是Wikimedia Commons社区选出的网站上的最高质量的图片中的一部分,是来自于技术高超的摄影师和绘画师。
通过附近位置上传的图片是指那些使用地图上发现位置功能上传的图片
这些功能允许编辑人员给那些做出了有用编辑的用户发送感谢通知-感谢通知通过使用在历史页面或差分页面上的一个小的感谢链接实现的。
+ 复制到后续媒体
+ 已复制
上传好图片到Commons的例子
不能上传图片的例子
跳过该图片
@@ -493,7 +503,20 @@
标志
由于他是
正在尝试更新分类。
+ 分类更新
成功
+
+ - 分类%1$s已添加。
+ - 分类%1$s已添加。
+
+ 无法添加分类。
+ 更新分类
+ 尝试更新坐标。
+ 坐标更新
+ 完成
+ 坐标 %1$s 已被添加。
+ 无法添加坐标。
+ 无法获取坐标。
分享图片透过
您目前还没有作出任何贡献
账户已创建!
@@ -521,31 +544,81 @@
跟随系统
暗黑
明亮
+ 无法打开位置设置。请手动打开位置
+ 选择高精确度模式以获得最佳结果。
打开位置?
+ 附近需要启用位置才能正常工作
+ 您是在同一地点拍摄这两张图片的吗?您想要使用右侧图片的纬度/经度吗?
加载更多
+ 未找到位置,请尝试更改您的搜索条件。
+ 建议的改进措施:
+ - 为此图像添加分类以提升可用性。
+ - 将此图像添加至相关的无图像的维基百科条目中。
将图片添加到维基百科
+ 您想将此图像添加至$1s维基百科条目中吗?
确认
说明
+ 1. 使用下列维基文本:
+ 2. 点击确认将会打开维基百科条目
+ 3. 寻找条目中适合您的图像的章节
+ 4. 点击该章节的编辑图标(类似铅笔形状的图标)。
+ 5. 将维基文本粘贴到合适的位置。
+ 6. 如果必要,在合适的位置编辑维基文本。参阅<a href=\"https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images#How_to_place_an_image\">此处</a>以获得更多信息。
+ 7. 发布条目
复制维基代码到剪贴板
暂停
继续
已暂停
更多
+ 我的收藏
+ 成就
排行榜
排名:
+ 数量:
排名
用户
计数
+ 设为排行榜头像
+ 正在设为头像,请稍候
+ 头像已设定
+ 设定新头像出错,请重试
+ 设为头像
+ 每年
+ 每周
所有时间
上传
附近
+ 已使用
我的排名
+ Mapbox Telemetry
+ 使用附近功能时向Mapbox发送匿名位置和使用数据
+ © <a href=\"https://www.mapbox.com/about/maps/\">Mapbox</a> © <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> <a href=\"https://www.mapbox.com/map-feedback/\">改进此地图</a>
+ 限制连接模式已启用!
+ 限制连接模式已禁用。挂起的上传将会立即恢复。
限制连接模式
+ 品质图像
+ 品质图像是符合一定质量标准(本质上大多是技术性的)的图表或照片,它们对维基媒体计划很有价值
正在恢复上传...
暂停上传...
+ 您已启用限制连接模式。所有的上传已暂停并将在您禁用此模式后立刻恢复。
+ 限制连接模式启用中。
+ 请编写一个简短的标题来介绍您的图片展示了什么。请介绍该图片是如何有趣、典型或罕见,并说明背景以及是否可见。请尽可能使用准确的术语。
+ 请寻找和选择此图像描绘的所有概念。越精确越好。如果图像描绘了多个项目,请在合理范围内全部选择。如果有更精确的标签可用,就不要选择通用标签。
+ 请选择合适的分类。与描述不同,分类仅使用英文。
+ 维基共享资源让您的图片能够重复使用并且供任何人修改。您想放弃所有使用权吗?您希望被标示出吗?您想依据相同的授权条款来修改内容吗?
描述
+ 媒体授权条款
媒体详情
查看分类页面
查看项目页面
+ 应用程序用户界面语言
+ 移除标题和描述
阅读更多
+ 在所有语言中
+ 选择一个地点
+ 放大和做小来调整
+ 退出位置拾取器
+ 选择地点
+ 位置拾取器的图像视图
+ 地点拾取器的照片观测视野的阴影。
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 688a73376..6983f6744 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -50,6 +50,7 @@
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c557b3268..2585ddfbe 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -622,13 +622,19 @@ Upload your first media by tapping on the add button.
App user interface language
Removes a caption and description
Read more
+ Custom Selector
+ No Images
+ In all languages
Choose a location
Pan and zoom to adjust
Exit location picker
Select location
- Location picker image view
- Location picker_image_view_shadow
- Custom Selector
- No Images
+ Show in map app
+ Edit location
+ The image view of the location picker
+
+ The shadow of the image view of the location picker
+ Image Location
+ Check whether location is correct
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index d3fb0bd67..1ae9e0a7c 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -19,6 +19,7 @@
- @color/button_blue_dark
- @color/white
- @color/white
+ - @color/white
- @color/commons_app_blue_dark
- @color/sub_background_dark
@@ -78,6 +79,7 @@
- @color/button_blue
- @color/black
- @color/black
+ - @color/black
- @color/commons_app_blue_light
- @color/sub_background_light
@@ -210,6 +212,12 @@
- bold
+
+
diff --git a/app/src/test/java/android/text/TextUtils.java b/app/src/test/java/android/text/TextUtils.java
index 0cb57cdd5..4d63e77df 100644
--- a/app/src/test/java/android/text/TextUtils.java
+++ b/app/src/test/java/android/text/TextUtils.java
@@ -6,7 +6,7 @@ import androidx.annotation.Nullable;
/**
* This Class Mocks TextUtils for the purpose of testing.
* NOTE: This class would not change the function of the TextUtils used in app
- * it onlt mocks it for the unit tests
+ * it only mocks it for the unit tests
*
*/
public class TextUtils {
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/ShadowActionBar.java b/app/src/test/kotlin/fr/free/nrw/commons/ShadowActionBar.java
index 26cec296b..a12858688 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/ShadowActionBar.java
+++ b/app/src/test/kotlin/fr/free/nrw/commons/ShadowActionBar.java
@@ -8,14 +8,14 @@ import org.robolectric.annotation.Implements;
@Implements(ActionBar.class)
public class ShadowActionBar {
- private boolean showHomeAsUp;
+ private boolean showHomeAsUp;
- public boolean getShowHomeAsUp() {
- return showHomeAsUp;
- }
+ public boolean getShowHomeAsUp() {
+ return showHomeAsUp;
+ }
- @Implementation
- void setDisplayHomeAsUpEnabled(final boolean showHomeAsUp) {
- this.showHomeAsUp = showHomeAsUp;
- }
+ @Implementation
+ void setDisplayHomeAsUpEnabled(final boolean showHomeAsUp) {
+ this.showHomeAsUp = showHomeAsUp;
+ }
}
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestAppAdapter.java b/app/src/test/kotlin/fr/free/nrw/commons/TestAppAdapter.java
index 39bcc515a..1b61a6fb7 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/TestAppAdapter.java
+++ b/app/src/test/kotlin/fr/free/nrw/commons/TestAppAdapter.java
@@ -12,60 +12,60 @@ import org.wikipedia.login.LoginResult;
public class TestAppAdapter extends AppAdapter {
- @Override
- public String getMediaWikiBaseUrl() {
- return Service.WIKIPEDIA_URL;
- }
+ @Override
+ public String getMediaWikiBaseUrl() {
+ return Service.WIKIPEDIA_URL;
+ }
- @Override
- public String getRestbaseUriFormat() {
- return "%1$s://%2$s/api/rest_v1/";
- }
+ @Override
+ public String getRestbaseUriFormat() {
+ return "%1$s://%2$s/api/rest_v1/";
+ }
- @Override
- public OkHttpClient getOkHttpClient(@NonNull WikiSite wikiSite) {
- return new OkHttpClient.Builder()
- .addInterceptor(new UnsuccessfulResponseInterceptor())
- .addInterceptor(new TestStubInterceptor())
- .build();
- }
+ @Override
+ public OkHttpClient getOkHttpClient(@NonNull WikiSite wikiSite) {
+ return new OkHttpClient.Builder()
+ .addInterceptor(new UnsuccessfulResponseInterceptor())
+ .addInterceptor(new TestStubInterceptor())
+ .build();
+ }
- @Override
- public int getDesiredLeadImageDp() {
- return 0;
- }
+ @Override
+ public int getDesiredLeadImageDp() {
+ return 0;
+ }
- @Override
- public boolean isLoggedIn() {
- return false;
- }
+ @Override
+ public boolean isLoggedIn() {
+ return false;
+ }
- @Override
- public String getUserName() {
- return null;
- }
+ @Override
+ public String getUserName() {
+ return null;
+ }
- @Override
- public String getPassword() {
- return null;
- }
+ @Override
+ public String getPassword() {
+ return null;
+ }
- @Override
- public void updateAccount(@NonNull LoginResult result) {
- }
+ @Override
+ public void updateAccount(@NonNull LoginResult result) {
+ }
- @Override
- public SharedPreferenceCookieManager getCookies() {
- return null;
- }
+ @Override
+ public SharedPreferenceCookieManager getCookies() {
+ return null;
+ }
- @Override
- public void setCookies(@NonNull SharedPreferenceCookieManager cookies) {
- }
+ @Override
+ public void setCookies(@NonNull SharedPreferenceCookieManager cookies) {
+ }
- @Override
- public boolean logErrorsInsteadOfCrashing() {
- return false;
- }
+ @Override
+ public boolean logErrorsInsteadOfCrashing() {
+ return false;
+ }
}
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsFragmentUnitTests.kt
new file mode 100644
index 000000000..81cb2fd49
--- /dev/null
+++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsFragmentUnitTests.kt
@@ -0,0 +1,354 @@
+package fr.free.nrw.commons.contributions
+
+import android.content.Context
+import android.os.Bundle
+import android.os.Looper
+import android.view.*
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.TestAppAdapter
+import fr.free.nrw.commons.TestCommonsApplication
+import fr.free.nrw.commons.campaigns.Campaign
+import fr.free.nrw.commons.campaigns.CampaignView
+import fr.free.nrw.commons.kvstore.JsonKvStore
+import fr.free.nrw.commons.media.MediaDetailPagerFragment
+import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
+import fr.free.nrw.commons.nearby.NearbyNotificationCardView
+import fr.free.nrw.commons.notification.Notification
+import fr.free.nrw.commons.notification.NotificationController
+import fr.free.nrw.commons.notification.NotificationType
+import io.reactivex.Single
+import io.reactivex.disposables.CompositeDisposable
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.*
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.powermock.reflect.Whitebox
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import org.wikipedia.AppAdapter
+import java.lang.reflect.Method
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [21], application = TestCommonsApplication::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+class ContributionsFragmentUnitTests {
+
+ @Mock
+ private lateinit var mediaDetailPagerFragment: MediaDetailPagerFragment
+
+ @Mock
+ private lateinit var contributionsListFragment: ContributionsListFragment
+
+ @Mock
+ private lateinit var layoutInflater: LayoutInflater
+
+ @Mock
+ private lateinit var menuInflater: MenuInflater
+
+ @Mock
+ private lateinit var menu: Menu
+
+ @Mock
+ private lateinit var menuItem: MenuItem
+
+ @Mock
+ private lateinit var notification: View
+
+ @Mock
+ private lateinit var store: JsonKvStore
+
+ @Mock
+ private lateinit var notificationController: NotificationController
+
+ @Mock
+ private lateinit var limitedConnectionEnabledLayout: LinearLayout
+
+ @Mock
+ private lateinit var notificationCount: TextView
+
+ @Mock
+ private lateinit var singleNotification: Single>
+
+ @Mock
+ private lateinit var compositeDisposable: CompositeDisposable
+
+ @Mock
+ private lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
+
+ private lateinit var fragment: ContributionsFragment
+ private lateinit var context: Context
+ private lateinit var view: View
+ private lateinit var activity: MainActivity
+ private lateinit var nearbyNotificationCardView: NearbyNotificationCardView
+ private lateinit var campaignView: CampaignView
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ AppAdapter.set(TestAppAdapter())
+
+ context = RuntimeEnvironment.application.applicationContext
+ activity = Robolectric.buildActivity(MainActivity::class.java).create().get()
+
+
+ fragment = ContributionsFragment.newInstance()
+ val fragmentManager: FragmentManager = activity.supportFragmentManager
+ val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
+ fragmentTransaction.add(fragment, null)
+ fragmentTransaction.commit()
+
+ layoutInflater = LayoutInflater.from(activity)
+ view = LayoutInflater.from(activity)
+ .inflate(R.layout.fragment_contributions, null) as View
+
+ nearbyNotificationCardView = view.findViewById(R.id.card_view_nearby)
+ campaignView = view.findViewById(R.id.campaigns_view)
+
+ Whitebox.setInternalState(fragment, "contributionsListFragment", contributionsListFragment)
+ Whitebox.setInternalState(fragment, "store", store)
+ Whitebox.setInternalState(
+ fragment,
+ "limitedConnectionEnabledLayout",
+ limitedConnectionEnabledLayout
+ )
+ Whitebox.setInternalState(fragment, "notificationCount", notificationCount)
+ Whitebox.setInternalState(fragment, "notificationController", notificationController)
+ Whitebox.setInternalState(fragment, "compositeDisposable", compositeDisposable)
+ Whitebox.setInternalState(fragment, "okHttpJsonApiClient", okHttpJsonApiClient)
+ Whitebox.setInternalState(
+ fragment,
+ "nearbyNotificationCardView",
+ nearbyNotificationCardView
+ )
+ Whitebox.setInternalState(fragment, "campaignView", campaignView)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun checkFragmentNotNull() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Assert.assertNotNull(fragment)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testGetMediaDetailPagerFragment() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "mediaDetailPagerFragment", mediaDetailPagerFragment)
+ Assert.assertEquals(fragment.mediaDetailPagerFragment, mediaDetailPagerFragment)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnCreateOptionsMenu() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ `when`(menu.findItem(anyInt())).thenReturn(menuItem)
+ `when`(menuItem.actionView).thenReturn(notification)
+ fragment.onCreateOptionsMenu(menu, menuInflater)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testSetNotificationCount() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ `when`(notificationController.getNotifications(anyBoolean())).thenReturn(singleNotification)
+ `when`(notificationController.getNotifications(anyBoolean()).subscribeOn(any())).thenReturn(
+ singleNotification
+ )
+ `when`(
+ notificationController.getNotifications(anyBoolean()).subscribeOn(any()).observeOn(
+ any()
+ )
+ ).thenReturn(singleNotification)
+ `when`(
+ notificationController.getNotifications(anyBoolean()).subscribeOn(any()).observeOn(
+ any()
+ ).subscribe()
+ ).thenReturn(compositeDisposable)
+ fragment.setNotificationCount()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testInitNotificationViewsCaseEmptyList() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val method: Method = ContributionsFragment::class.java.getDeclaredMethod(
+ "initNotificationViews",
+ List::class.java
+ )
+ method.isAccessible = true
+ method.invoke(fragment, listOf())
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testInitNotificationViewsCaseNonEmptyList() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val list: List =
+ listOf(Notification(NotificationType.UNKNOWN, "", "", "", "", ""))
+ val method: Method = ContributionsFragment::class.java.getDeclaredMethod(
+ "initNotificationViews",
+ List::class.java
+ )
+ method.isAccessible = true
+ method.invoke(fragment, list)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testUpdateLimitedConnectionToggleCaseIsEnabled() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ `when`(menu.findItem(anyInt())).thenReturn(menuItem)
+ `when`(menuItem.actionView).thenReturn(notification)
+ `when`(store.getBoolean(anyString(), anyBoolean())).thenReturn(true)
+ fragment.updateLimitedConnectionToggle(menu)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnAttach() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onAttach(context)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testNotifyDataSetChanged() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "mediaDetailPagerFragment", mediaDetailPagerFragment)
+ fragment.notifyDataSetChanged()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnDestroyView() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onDestroyView()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowMessage() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showMessage("")
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowCampaigns() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showCampaigns(Campaign("", "", "2000-01-01", "2000-01-02", ""))
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnPause() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onPause()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnSaveInstanceState() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onSaveInstanceState(Bundle())
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnViewCreated() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onViewCreated(campaignView, Bundle())
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnResumeCaseNonNull() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "mediaDetailPagerFragment", mediaDetailPagerFragment)
+ fragment.onResume()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnResumeCaseNullReady() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onResume()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnResumeCaseNullCaseIf() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ nearbyNotificationCardView.cardViewVisibilityState =
+ NearbyNotificationCardView.CardViewVisibilityState.READY
+ fragment.onResume()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnResumeCaseNullCaseElse() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ `when`(store.getBoolean("displayNearbyCardView", true)).thenReturn(false)
+ fragment.onResume()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testGetContributionStateAt() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.getContributionStateAt(0)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testViewPagerNotifyDataSetChanged() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "mediaDetailPagerFragment", mediaDetailPagerFragment)
+ fragment.viewPagerNotifyDataSetChanged()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testGetMediaAtPosition() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.getMediaAtPosition(0)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testGetTotalMediaCount() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.totalMediaCount
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testRefreshNominatedMedia() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.refreshNominatedMedia(0)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowDetail() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "mediaDetailPagerFragment", mediaDetailPagerFragment)
+ `when`(mediaDetailPagerFragment.isVisible).thenReturn(false)
+ fragment.showDetail(0, false)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/depictions/DepictsClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/depictions/DepictsClientTest.kt
index 237fb49f4..b0f4fcf2e 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/explore/depictions/DepictsClientTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/depictions/DepictsClientTest.kt
@@ -10,6 +10,7 @@ import fr.free.nrw.commons.upload.depicts.DepictsInterface
import fr.free.nrw.commons.wikidata.model.DepictSearchResponse
import io.reactivex.Single
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@@ -28,6 +29,7 @@ class DepictsClientTest {
}
@Test
+ @Ignore()
fun searchForDepictions() {
val depictSearchResponse = mock()
whenever(depictsInterface.searchForDepicts("query", "1", "en", "en", "0"))
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt
index 65eaeb1c6..478fc6b53 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt
@@ -46,13 +46,6 @@ class MoreBottomSheetLoggedOutFragmentUnitTests {
Assert.assertNotNull(fragment)
}
- @Test
- @Throws(Exception::class)
- fun testOnTutorialClicked() {
- Shadows.shadowOf(Looper.getMainLooper()).idle()
- fragment.onTutorialClicked()
- }
-
@Test
@Throws(Exception::class)
fun testOnSettingsClicked() {
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizActivityUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizActivityUnitTest.kt
index 0ac5ff96e..2750a3adc 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizActivityUnitTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizActivityUnitTest.kt
@@ -3,7 +3,7 @@ package fr.free.nrw.commons.quiz
import android.content.Context
import android.view.LayoutInflater
import android.view.View
-import android.widget.RadioButton
+import android.widget.Button
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.soloader.SoLoader
import fr.free.nrw.commons.R
@@ -30,8 +30,8 @@ class QuizActivityUnitTest {
private val SAMPLE_ALERT_MESSAGE_VALUE = "Message"
private lateinit var activity: QuizActivity
- private lateinit var positiveAnswer: RadioButton
- private lateinit var negativeAnswer: RadioButton
+ private lateinit var positiveAnswer: Button
+ private lateinit var negativeAnswer: Button
private lateinit var view: View
private lateinit var context: Context
@@ -71,13 +71,6 @@ class QuizActivityUnitTest {
activity.setNextQuestion()
}
- @Test
- @Throws(Exception::class)
- fun testSetNextQuestion() {
- activity.negativeAnswer.isChecked = true
- activity.setNextQuestion()
- }
-
@Test
@Throws(Exception::class)
fun testOnBackPressed() {
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt
new file mode 100644
index 000000000..c7817f5d3
--- /dev/null
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt
@@ -0,0 +1,256 @@
+package fr.free.nrw.commons.upload.categories
+
+import android.content.Context
+import android.os.Looper
+import android.text.Editable
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import android.widget.ProgressBar
+import android.widget.TextView
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.textfield.TextInputLayout
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.TestAppAdapter
+import fr.free.nrw.commons.TestCommonsApplication
+import fr.free.nrw.commons.upload.UploadActivity
+import fr.free.nrw.commons.upload.UploadBaseFragment
+import io.reactivex.disposables.Disposable
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.powermock.reflect.Whitebox
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import org.wikipedia.AppAdapter
+import java.lang.reflect.Method
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [21], application = TestCommonsApplication::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+class UploadCategoriesFragmentUnitTests {
+
+ private lateinit var fragment: UploadCategoriesFragment
+ private lateinit var context: Context
+ private lateinit var fragmentManager: FragmentManager
+ private lateinit var layoutInflater: LayoutInflater
+ private lateinit var view: View
+
+ @Mock
+ private lateinit var subscribe: Disposable
+
+ @Mock
+ private lateinit var pbCategories: ProgressBar
+
+ @Mock
+ private lateinit var tilContainerEtSearch: TextInputLayout
+
+ @Mock
+ private lateinit var etSearch: TextInputEditText
+
+ @Mock
+ private lateinit var rvCategories: RecyclerView
+
+ @Mock
+ private lateinit var tvTitle: TextView
+
+ @Mock
+ private lateinit var tooltip: ImageView
+
+ @Mock
+ private lateinit var editable: Editable
+
+ @Mock
+ private lateinit var adapter: UploadCategoryAdapter
+
+ @Mock
+ private lateinit var callback: UploadBaseFragment.Callback
+
+ @Mock
+ private lateinit var presenter: CategoriesContract.UserActionListener
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ context = RuntimeEnvironment.application.applicationContext
+ AppAdapter.set(TestAppAdapter())
+ val activity = Robolectric.buildActivity(UploadActivity::class.java).create().get()
+ fragment = UploadCategoriesFragment()
+ fragmentManager = activity.supportFragmentManager
+ val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
+ fragmentTransaction.add(fragment, null)
+ fragmentTransaction.commit()
+ layoutInflater = LayoutInflater.from(activity)
+ view = LayoutInflater.from(activity)
+ .inflate(R.layout.upload_categories_fragment, null) as View
+ Whitebox.setInternalState(fragment, "subscribe", subscribe)
+ Whitebox.setInternalState(fragment, "pbCategories", pbCategories)
+ Whitebox.setInternalState(fragment, "tilContainerEtSearch", tilContainerEtSearch)
+ Whitebox.setInternalState(fragment, "adapter", adapter)
+ Whitebox.setInternalState(fragment, "callback", callback)
+ Whitebox.setInternalState(fragment, "presenter", presenter)
+ Whitebox.setInternalState(fragment, "etSearch", etSearch)
+ Whitebox.setInternalState(fragment, "rvCategories", rvCategories)
+ Whitebox.setInternalState(fragment, "tvTitle", tvTitle)
+ Whitebox.setInternalState(fragment, "tooltip", tooltip)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun checkFragmentNotNull() {
+ Assert.assertNotNull(fragment)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnCreateView() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onCreateView(layoutInflater,null, null)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnViewCreated() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onViewCreated(view, null)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnDestroyView() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onDestroyView()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowProgress() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showProgress(true)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowErrorString() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showError("")
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowErrorInt() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showError(R.string.no_categories_found)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testSetCategoriesCaseNull() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.setCategories(null)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testSetCategoriesCaseNonNull() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.setCategories(listOf())
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testGoToNextScreen() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.goToNextScreen()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowNoCategorySelected() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showNoCategorySelected()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnNextButtonClicked() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onNextButtonClicked()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnPreviousButtonClicked() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onPreviousButtonClicked()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnBecameVisible() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ `when`(etSearch.text).thenReturn(editable)
+ val method: Method = UploadCategoriesFragment::class.java.getDeclaredMethod(
+ "onBecameVisible"
+ )
+ method.isAccessible = true
+ method.invoke(fragment)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testAddTextChangeListenerToEtSearch() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val method: Method = UploadCategoriesFragment::class.java.getDeclaredMethod(
+ "addTextChangeListenerToEtSearch"
+ )
+ method.isAccessible = true
+ method.invoke(fragment)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testSearchForCategory() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val method: Method = UploadCategoriesFragment::class.java.getDeclaredMethod(
+ "searchForCategory",
+ String::class.java
+ )
+ method.isAccessible = true
+ method.invoke(fragment, "")
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testInitRecyclerView() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val method: Method = UploadCategoriesFragment::class.java.getDeclaredMethod(
+ "initRecyclerView"
+ )
+ method.isAccessible = true
+ method.invoke(fragment)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testInit() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val method: Method = UploadCategoriesFragment::class.java.getDeclaredMethod(
+ "init"
+ )
+ method.isAccessible = true
+ method.invoke(fragment)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt
new file mode 100644
index 000000000..93b392858
--- /dev/null
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt
@@ -0,0 +1,432 @@
+package fr.free.nrw.commons.upload.mediaDetails
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.appcompat.widget.AppCompatButton
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.recyclerview.widget.RecyclerView
+import com.github.chrisbanes.photoview.PhotoView
+import com.mapbox.mapboxsdk.camera.CameraPosition
+import com.mapbox.mapboxsdk.geometry.LatLng
+import com.nhaarman.mockitokotlin2.mock
+import fr.free.nrw.commons.LocationPicker.LocationPicker
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.TestAppAdapter
+import fr.free.nrw.commons.TestCommonsApplication
+import fr.free.nrw.commons.kvstore.JsonKvStore
+import fr.free.nrw.commons.nearby.Place
+import fr.free.nrw.commons.upload.ImageCoordinates
+import fr.free.nrw.commons.upload.UploadActivity
+import fr.free.nrw.commons.upload.UploadItem
+import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.powermock.reflect.Whitebox
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import org.wikipedia.AppAdapter
+import java.lang.reflect.Method
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [21], application = TestCommonsApplication::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+class UploadMediaDetailFragmentUnitTest {
+
+ private lateinit var fragment: UploadMediaDetailFragment
+ private lateinit var fragmentManager: FragmentManager
+ private lateinit var context: Context
+ private lateinit var layoutInflater: LayoutInflater
+ private lateinit var view: View
+
+ private lateinit var tvTitle: TextView
+ private lateinit var tooltip: ImageView
+ private lateinit var rvDescriptions: RecyclerView
+ private lateinit var btnPrevious: AppCompatButton
+ private lateinit var btnNext: AppCompatButton
+ private lateinit var btnCopyToSubsequentMedia: AppCompatButton
+ private lateinit var photoViewBackgroundImage: PhotoView
+ private lateinit var ibMap: AppCompatImageButton
+ private lateinit var llContainerMediaDetail: LinearLayout
+ private lateinit var ibExpandCollapse: AppCompatImageButton
+
+ @Mock
+ private lateinit var savedInstanceState: Bundle
+
+ @Mock
+ private lateinit var callback: UploadMediaDetailFragment.UploadMediaDetailFragmentCallback
+
+ @Mock
+ private lateinit var presenter: UploadMediaDetailsContract.UserActionListener
+
+ @Mock
+ private lateinit var uploadMediaDetailAdapter: UploadMediaDetailAdapter
+
+ @Mock
+ private lateinit var uploadItem: UploadItem
+
+ @Mock
+ private lateinit var mediaUri: Uri
+
+ @Mock
+ private lateinit var place: Place
+
+ @Mock
+ private lateinit var defaultKvStore: JsonKvStore
+
+ @Mock
+ private lateinit var imageCoordinates: ImageCoordinates
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ context = RuntimeEnvironment.application.applicationContext
+ AppAdapter.set(TestAppAdapter())
+
+ val activity = Robolectric.buildActivity(UploadActivity::class.java).create().get()
+ layoutInflater = LayoutInflater.from(activity)
+
+ view = LayoutInflater.from(activity)
+ .inflate(R.layout.fragment_upload_media_detail_fragment, null) as View
+
+ fragment = UploadMediaDetailFragment()
+ fragmentManager = activity.supportFragmentManager
+ val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
+ fragmentTransaction.add(fragment, null)
+ fragmentTransaction.commit()
+
+ tvTitle = view.findViewById(R.id.tv_title)
+ tooltip = view.findViewById(R.id.tooltip)
+ rvDescriptions = view.findViewById(R.id.rv_descriptions)
+ btnPrevious = view.findViewById(R.id.btn_previous)
+ btnNext = view.findViewById(R.id.btn_next)
+ btnCopyToSubsequentMedia = view.findViewById(R.id.btn_copy_subsequent_media)
+ photoViewBackgroundImage = view.findViewById(R.id.backgroundImage)
+ ibMap = view.findViewById(R.id.ib_map)
+ llContainerMediaDetail = view.findViewById(R.id.ll_container_media_detail)
+ ibExpandCollapse = view.findViewById(R.id.ib_expand_collapse)
+
+ Whitebox.setInternalState(fragment, "tvTitle", tvTitle)
+ Whitebox.setInternalState(fragment, "tooltip", tooltip)
+ Whitebox.setInternalState(fragment, "callback", callback)
+ Whitebox.setInternalState(fragment, "rvDescriptions", rvDescriptions)
+ Whitebox.setInternalState(fragment, "btnPrevious", btnPrevious)
+ Whitebox.setInternalState(fragment, "btnNext", btnNext)
+ Whitebox.setInternalState(fragment, "btnCopyToSubsequentMedia", btnCopyToSubsequentMedia)
+ Whitebox.setInternalState(fragment, "photoViewBackgroundImage", photoViewBackgroundImage)
+ Whitebox.setInternalState(fragment, "uploadMediaDetailAdapter", uploadMediaDetailAdapter)
+ Whitebox.setInternalState(fragment, "ibMap", ibMap)
+ Whitebox.setInternalState(fragment, "llContainerMediaDetail", llContainerMediaDetail)
+ Whitebox.setInternalState(fragment, "ibExpandCollapse", ibExpandCollapse)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun checkFragmentNotNull() {
+ Assert.assertNotNull(fragment)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testSetCallback() {
+ fragment.setCallback(null)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnCreate() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onCreate(Bundle())
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testSetImageTobeUploaded() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.setImageTobeUploaded(null, null)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnCreateView() {
+ fragment.onCreateView(layoutInflater, null, savedInstanceState)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnViewCreated() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "presenter", presenter)
+ fragment.onViewCreated(view, savedInstanceState)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testInit() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "presenter", presenter)
+ val method: Method = UploadMediaDetailFragment::class.java.getDeclaredMethod(
+ "init"
+ )
+ method.isAccessible = true
+ method.invoke(fragment)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testInitCaseGetIndexInViewFlipperNonZero() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "presenter", presenter)
+ `when`(callback.getIndexInViewFlipper(fragment)).thenReturn(1)
+ `when`(callback.totalNumberOfSteps).thenReturn(5)
+ val method: Method = UploadMediaDetailFragment::class.java.getDeclaredMethod(
+ "init"
+ )
+ method.isAccessible = true
+ method.invoke(fragment)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowInfoAlert() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val method: Method = UploadMediaDetailFragment::class.java.getDeclaredMethod(
+ "showInfoAlert", Int::class.java, Int::class.java
+ )
+ method.isAccessible = true
+ method.invoke(fragment, R.string.media_detail_step_title, R.string.media_details_tooltip)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnNextButtonClicked() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "presenter", presenter)
+ fragment.onNextButtonClicked()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnPreviousButtonClicked() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "presenter", presenter)
+ fragment.onPreviousButtonClicked()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnButtonAddDescriptionClicked() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onButtonAddDescriptionClicked()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowSimilarImageFragment() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val similar: ImageCoordinates = mock()
+ fragment.showSimilarImageFragment("original", "possible", similar)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnImageProcessed() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ `when`(uploadItem.mediaUri).thenReturn(mediaUri)
+ fragment.onImageProcessed(uploadItem, place)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnNearbyPlaceFound() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onNearbyPlaceFound(uploadItem, place)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowProgress() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showProgress(true)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnImageValidationSuccess() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onImageValidationSuccess()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnBecameVisible() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "presenter", presenter)
+ Whitebox.setInternalState(fragment, "showNearbyFound", true)
+ Whitebox.setInternalState(fragment, "nearbyPlace", place)
+ Whitebox.setInternalState(fragment, "uploadItem", uploadItem)
+ val method: Method = UploadMediaDetailFragment::class.java.getDeclaredMethod(
+ "onBecameVisible"
+ )
+ method.isAccessible = true
+ method.invoke(fragment)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowMessageCaseOne() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showMessage(R.string.add_caption_toast, R.color.color_error)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowMessageCaseTwo() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showMessage("", R.color.color_error)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowDuplicatePicturePopupCaseDefault() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showDuplicatePicturePopup(uploadItem)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowDuplicatePicturePopupCaseFalse() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "defaultKvStore", defaultKvStore)
+ Whitebox.setInternalState(fragment, "presenter", presenter)
+ `when`(defaultKvStore.getBoolean("showDuplicatePicturePopup", true)).thenReturn(false)
+ fragment.showDuplicatePicturePopup(uploadItem)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowBadImagePopup() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showBadImagePopup(8, uploadItem)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowConnectionErrorPopup() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showConnectionErrorPopup()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowMapWithImageCoordinates() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.showMapWithImageCoordinates(true)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testShowExternalMap() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates)
+ `when`(imageCoordinates.decLatitude).thenReturn(0.0)
+ `when`(imageCoordinates.decLongitude).thenReturn(0.0)
+ fragment.showExternalMap(uploadItem)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnActivityResult() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Mockito.mock(LocationPicker::class.java)
+ val intent = Mockito.mock(Intent::class.java)
+ val cameraPosition = Mockito.mock(CameraPosition::class.java)
+ val latLng = Mockito.mock(LatLng::class.java)
+
+ Whitebox.setInternalState(cameraPosition, "target", latLng)
+ Whitebox.setInternalState(fragment, "editableUploadItem", uploadItem)
+
+ `when`(LocationPicker.getCameraPosition(intent)).thenReturn(cameraPosition)
+ `when`(latLng.latitude).thenReturn(0.0)
+ `when`(latLng.longitude).thenReturn(0.0)
+ `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates)
+ fragment.onActivityResult(1211, Activity.RESULT_OK, intent)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testUpdateMediaDetails() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.updateMediaDetails(null)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testDeleteThisPicture() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val method: Method = UploadMediaDetailFragment::class.java.getDeclaredMethod(
+ "deleteThisPicture"
+ )
+ method.isAccessible = true
+ method.invoke(fragment)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnDestroyView() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onDestroyView()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnRlContainerTitleClicked() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onRlContainerTitleClicked()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnIbMapClicked() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "presenter", presenter)
+ fragment.onIbMapClicked()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnPrimaryCaptionTextChange() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ fragment.onPrimaryCaptionTextChange(false)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testOnButtonCopyTitleDescToSubsequentMedia() {
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ Whitebox.setInternalState(fragment, "presenter", presenter)
+ fragment.onButtonCopyTitleDescToSubsequentMedia()
+ }
+
+}
\ No newline at end of file
diff --git a/data-client/src/main/java/org/wikipedia/gallery/ImageInfo.java b/data-client/src/main/java/org/wikipedia/gallery/ImageInfo.java
index 4b8e63ad4..6ce92b0ff 100644
--- a/data-client/src/main/java/org/wikipedia/gallery/ImageInfo.java
+++ b/data-client/src/main/java/org/wikipedia/gallery/ImageInfo.java
@@ -29,6 +29,16 @@ public class ImageInfo implements Serializable {
@Nullable private String user;
@Nullable private String timestamp;
+ /**
+ * Query width, default width parameter of the API query in pixels.
+ */
+ final private static int QUERY_WIDTH = 640;
+
+ /**
+ * Threshold height, the minimum height of the image in pixels.
+ */
+ final private static int THRESHOLD_HEIGHT = 220;
+
@NonNull
public String getSource() {
return StringUtils.defaultString(source);
@@ -50,11 +60,24 @@ public class ImageInfo implements Serializable {
return height;
}
+ /**
+ * Get the thumbnail width.
+ * @return
+ */
+ public int getThumbWidth() { return thumbWidth; }
+
+ /**
+ * Get the thumbnail height.
+ * @return
+ */
+ public int getThumbHeight() { return thumbHeight; }
+
@NonNull public String getMimeType() {
return StringUtils.defaultString(mimeType, "*/*");
}
@NonNull public String getThumbUrl() {
+ updateThumbUrl();
return StringUtils.defaultString(thumbUrl);
}
@@ -73,4 +96,26 @@ public class ImageInfo implements Serializable {
@Nullable public ExtMetadata getMetadata() {
return metadata;
}
+
+ /**
+ * Updates the ThumbUrl if image dimensions are not sufficient.
+ * Specifically, in panoramic images the height retrieved is less than required due to large width to height ratio,
+ * so we update the thumb url keeping a minimum height threshold.
+ */
+ private void updateThumbUrl() {
+ // If thumbHeight retrieved from API is less than THRESHOLD_HEIGHT
+ if(getThumbHeight() < THRESHOLD_HEIGHT){
+ // If thumbWidthRetrieved is same as queried width ( If not tells us that the image has no larger dimensions. )
+ if(getThumbWidth() == QUERY_WIDTH){
+ // Calculate new width depending on the aspect ratio.
+ final int finalWidth = (int)(THRESHOLD_HEIGHT * getThumbWidth() * 1.0 / getThumbHeight());
+ thumbHeight = THRESHOLD_HEIGHT;
+ thumbWidth = finalWidth;
+ final String toReplace = "/" + QUERY_WIDTH + "px";
+ final int position = thumbUrl.lastIndexOf(toReplace);
+ thumbUrl = (new StringBuilder(thumbUrl)).replace(position, position + toReplace.length(), "/" + thumbWidth + "px").toString();
+ }
+ }
+ }
+
}