diff --git a/app/build.gradle b/app/build.gradle
index fac1ab9ba..718057816 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -28,6 +28,7 @@ dependencies {
implementation 'com.facebook.fresco:fresco:1.13.0'
implementation 'com.drewnoakes:metadata-extractor:2.11.0'
implementation 'com.dmitrybrant:wikimedia-android-data-client:0.0.18'
+ implementation 'org.apache.commons:commons-lang3:3.8.1'
// UI
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
@@ -64,6 +65,8 @@ dependencies {
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'com.nhaarman:mockito-kotlin:1.5.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
+ testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5"
+ testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
// Android testing
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index dffea644c..b2e434bd4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -50,10 +50,13 @@
-
+ android:windowSoftInputMode="adjustResize"
+ >
diff --git a/app/src/main/java/fr/free/nrw/commons/BasePresenter.java b/app/src/main/java/fr/free/nrw/commons/BasePresenter.java
index 041fde6b2..2aa160520 100644
--- a/app/src/main/java/fr/free/nrw/commons/BasePresenter.java
+++ b/app/src/main/java/fr/free/nrw/commons/BasePresenter.java
@@ -3,11 +3,11 @@ package fr.free.nrw.commons;
/**
* Base presenter, enforcing contracts to atach and detach view
*/
-public interface BasePresenter {
+public interface BasePresenter {
/**
* Until a view is attached, it is open to listen events from the presenter
*/
- void onAttachView(MvpView view);
+ void onAttachView(T view);
/**
* Detaching a view makes sure that the view no more receives events from the presenter
diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java
index 43c708460..b7e09d9c2 100644
--- a/app/src/main/java/fr/free/nrw/commons/Utils.java
+++ b/app/src/main/java/fr/free/nrw/commons/Utils.java
@@ -11,6 +11,8 @@ import android.widget.Toast;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.page.PageTitle;
+import fr.free.nrw.commons.location.LatLng;
+import fr.free.nrw.commons.utils.ViewUtil;
import java.util.Locale;
import java.util.regex.Pattern;
@@ -18,9 +20,7 @@ import java.util.regex.Pattern;
import androidx.annotation.NonNull;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.content.ContextCompat;
-import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.settings.Prefs;
-import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
import static android.widget.Toast.LENGTH_SHORT;
diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java
index 0ac5ec8a7..4aa062718 100644
--- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java
+++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java
@@ -28,7 +28,7 @@ import timber.log.Timber;
* success and error
*/
@Singleton
-public class CampaignsPresenter implements BasePresenter {
+public class CampaignsPresenter implements BasePresenter {
private final OkHttpJsonApiClient okHttpJsonApiClient;
private ICampaignsView view;
@@ -40,8 +40,9 @@ public class CampaignsPresenter implements BasePresenter {
this.okHttpJsonApiClient = okHttpJsonApiClient;
}
- @Override public void onAttachView(MvpView view) {
- this.view = (ICampaignsView) view;
+ @Override
+ public void onAttachView(ICampaignsView view) {
+ this.view = view;
}
@Override public void onDetachView() {
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java
index 68f53ca36..9b084da49 100644
--- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java
@@ -1,25 +1,25 @@
package fr.free.nrw.commons.category;
import android.text.TextUtils;
-
+import fr.free.nrw.commons.kvstore.JsonKvStore;
+import fr.free.nrw.commons.mwapi.MediaWikiApi;
+import fr.free.nrw.commons.upload.GpsCategoryModel;
+import fr.free.nrw.commons.utils.StringSortingUtils;
+import io.reactivex.Observable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
-
import javax.inject.Inject;
import javax.inject.Named;
-
-import fr.free.nrw.commons.kvstore.JsonKvStore;
-import fr.free.nrw.commons.mwapi.MediaWikiApi;
-import fr.free.nrw.commons.upload.GpsCategoryModel;
-import fr.free.nrw.commons.utils.StringSortingUtils;
-import io.reactivex.Observable;
import timber.log.Timber;
-public class CategoriesModel implements CategoryClickedListener {
+/**
+ * The model class for categories in upload
+ */
+public class CategoriesModel{
private static final int SEARCH_CATS_LIMIT = 25;
private final MediaWikiApi mwApi;
@@ -41,13 +41,22 @@ public class CategoriesModel implements CategoryClickedListener {
this.selectedCategories = new ArrayList<>();
}
- //region Misc. utility methods
+ /**
+ * Sorts CategoryItem by similarity
+ * @param filter
+ * @return
+ */
public Comparator sortBySimilarity(final String filter) {
Comparator stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
return (firstItem, secondItem) -> stringSimilarityComparator
.compare(firstItem.getName(), secondItem.getName());
}
+ /**
+ * Returns if the item contains an year
+ * @param item
+ * @return
+ */
public boolean containsYear(String item) {
//Check for current and previous year to exclude these categories from removal
Calendar now = Calendar.getInstance();
@@ -67,6 +76,10 @@ public class CategoriesModel implements CategoryClickedListener {
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
}
+ /**
+ * Updates category count in category dao
+ * @param item
+ */
public void updateCategoryCount(CategoryItem item) {
Category category = categoryDao.find(item.getName());
@@ -78,29 +91,27 @@ public class CategoriesModel implements CategoryClickedListener {
category.incTimesUsed();
categoryDao.save(category);
}
- //endregion
-
- //region Category Caching
- public void cacheAll(HashMap> categories) {
- categoriesCache.putAll(categories);
- }
-
- public HashMap> getCategoriesCache() {
- return categoriesCache;
- }
boolean cacheContainsKey(String term) {
return categoriesCache.containsKey(term);
}
//endregion
- //region Category searching
+ /**
+ * Regional category search
+ * @param term
+ * @param imageTitleList
+ * @return
+ */
public Observable searchAll(String term, List imageTitleList) {
- //If user hasn't typed anything in yet, get GPS and recent items
+ //If query text is empty, show him category based on gps and title and recent searches
if (TextUtils.isEmpty(term)) {
- return gpsCategories()
- .concatWith(titleCategories(imageTitleList))
- .concatWith(recentCategories());
+ Observable categoryItemObservable = gpsCategories()
+ .concatWith(titleCategories(imageTitleList));
+ if (hasDirectCategories()) {
+ categoryItemObservable.concatWith(directCategories().concatWith(recentCategories()));
+ }
+ return categoryItemObservable;
}
//if user types in something that is in cache, return cached category
@@ -115,43 +126,28 @@ public class CategoriesModel implements CategoryClickedListener {
.map(name -> new CategoryItem(name, false));
}
- public Observable searchCategories(String term, List imageTitleList) {
- //If user hasn't typed anything in yet, get GPS and recent items
- if (TextUtils.isEmpty(term)) {
- return gpsCategories()
- .concatWith(titleCategories(imageTitleList))
- .concatWith(recentCategories());
- }
-
- return mwApi
- .searchCategories(term, SEARCH_CATS_LIMIT)
- .map(s -> new CategoryItem(s, false));
- }
+ /**
+ * Returns cached categories
+ * @param term
+ * @return
+ */
private ArrayList getCachedCategories(String term) {
return categoriesCache.get(term);
}
- public Observable defaultCategories(List titleList) {
- Observable directCat = directCategories();
- if (hasDirectCategories()) {
- Timber.d("Image has direct Cat");
- return directCat
- .concatWith(gpsCategories())
- .concatWith(titleCategories(titleList))
- .concatWith(recentCategories());
- } else {
- Timber.d("Image has no direct Cat");
- return gpsCategories()
- .concatWith(titleCategories(titleList))
- .concatWith(recentCategories());
- }
- }
-
+ /**
+ * Returns if we have a category in DirectKV Store
+ * @return
+ */
private boolean hasDirectCategories() {
return !directKvStore.getString("Category", "").equals("");
}
+ /**
+ * Returns categories in DirectKVStore
+ * @return
+ */
private Observable directCategories() {
String directCategory = directKvStore.getString("Category", "");
List categoryList = new ArrayList<>();
@@ -164,30 +160,49 @@ public class CategoriesModel implements CategoryClickedListener {
return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
}
+ /**
+ * Returns GPS categories
+ * @return
+ */
Observable gpsCategories() {
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false));
}
+ /**
+ * Returns title based categories
+ * @param titleList
+ * @return
+ */
private Observable titleCategories(List titleList) {
return Observable.fromIterable(titleList)
.concatMap(this::getTitleCategories);
}
+ /**
+ * Return category for single title
+ * @param title
+ * @return
+ */
private Observable getTitleCategories(String title) {
return mwApi.searchTitles(title, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
+ /**
+ * Returns recent categories
+ * @return
+ */
private Observable recentCategories() {
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
.map(s -> new CategoryItem(s, false));
}
- //endregion
- //region Category Selection
- @Override
- public void categoryClicked(CategoryItem item) {
+ /**
+ * Handles category item selection
+ * @param item
+ */
+ public void onCategoryItemClicked(CategoryItem item) {
if (item.isSelected()) {
selectCategory(item);
updateCategoryCount(item);
@@ -196,22 +211,35 @@ public class CategoriesModel implements CategoryClickedListener {
}
}
+ /**
+ * Select's category
+ * @param item
+ */
public void selectCategory(CategoryItem item) {
selectedCategories.add(item);
}
+ /**
+ * Unselect Category
+ * @param item
+ */
public void unselectCategory(CategoryItem item) {
selectedCategories.remove(item);
}
- public int selectedCategoriesCount() {
- return selectedCategories.size();
- }
+ /**
+ * Get Selected Categories
+ * @return
+ */
public List getSelectedCategories() {
return selectedCategories;
}
+ /**
+ * Get Categories String List
+ * @return
+ */
public List getCategoryStringList() {
List output = new ArrayList<>();
for (CategoryItem item : selectedCategories) {
@@ -219,6 +247,12 @@ public class CategoriesModel implements CategoryClickedListener {
}
return output;
}
- //endregion
+ /**
+ * Cleanup the existing in memory cache's
+ */
+ public void cleanUp() {
+ this.categoriesCache.clear();
+ this.selectedCategories.clear();
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java
index f3ade09d8..f6c954f43 100644
--- a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java
@@ -19,7 +19,7 @@ public class CategoryItem implements Parcelable {
}
};
- CategoryItem(String name, boolean selected) {
+ public CategoryItem(String name, boolean selected) {
this.name = name;
this.selected = selected;
}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
index 2e9ca5327..ec02c7313 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
@@ -100,7 +100,7 @@ public class ContributionDao {
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
}
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength());
- //This was always meant to store the date created..If somehow date created is not fetched while actually saving the contribution, lets save today's date
+ //This was always meant to store the date created..If somehow date created is not fetched while actually saving the contribution, lets saveValue today's date
cv.put(Table.COLUMN_TIMESTAMP, contribution.getDateCreated()==null?System.currentTimeMillis():contribution.getDateCreated().getTime());
cv.put(Table.COLUMN_STATE, contribution.getState());
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());
diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java
index 0b0d8fde4..1b72833dd 100644
--- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java
+++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java
@@ -110,7 +110,7 @@ public class DeleteHelper {
mwApi.appendEdit(editToken, logPageString + "\n",
"Commons:Deletion_requests/" + date, summary);
mwApi.appendEdit(editToken, userPageString + "\n",
- "User_Talk:" + sessionManager.getCurrentAccount().name, summary);
+ "User_Talk:" + media.getCreator(), summary);
} catch (Exception e) {
Timber.e(e);
return false;
diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java
index e63f2b669..72793f2c8 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java
@@ -15,6 +15,7 @@ import fr.free.nrw.commons.nearby.PlaceRenderer;
import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.FileProcessor;
+import fr.free.nrw.commons.upload.UploadModule;
import fr.free.nrw.commons.widget.PicOfDayAppWidget;
@@ -27,7 +28,7 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget;
ActivityBuilderModule.class,
FragmentBuilderModule.class,
ServiceBuilderModule.class,
- ContentProviderBuilderModule.class
+ ContentProviderBuilderModule.class, UploadModule.class
})
public interface CommonsApplicationComponent extends AndroidInjector {
void inject(CommonsApplication application);
diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
index 7f0ee4048..36aba1668 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
@@ -9,6 +9,11 @@ import com.google.gson.Gson;
import org.wikipedia.dataclient.WikiSite;
+import io.reactivex.Scheduler;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+import org.wikipedia.dataclient.WikiSite;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -37,6 +42,8 @@ import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
@SuppressWarnings({"WeakerAccess", "unused"})
public class CommonsApplicationModule {
private Context applicationContext;
+ public static final String IO_THREAD="io_thread";
+ public static final String MAIN_THREAD="main_thread";
public CommonsApplicationModule(Context applicationContext) {
this.applicationContext = applicationContext;
@@ -172,4 +179,16 @@ public class CommonsApplicationModule {
public boolean provideIsBetaVariant() {
return ConfigUtils.isBetaFlavour();
}
+
+ @Named(IO_THREAD)
+ @Provides
+ public Scheduler providesIoThread(){
+ return Schedulers.io();
+ }
+
+ @Named(MAIN_THREAD)
+ @Provides
+ public Scheduler providesMainThread(){
+ return AndroidSchedulers.mainThread();
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
index 4350e08d9..74b022c42 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
@@ -18,6 +18,9 @@ import fr.free.nrw.commons.nearby.mvp.fragments.NearbyMapFragment;
import fr.free.nrw.commons.nearby.mvp.fragments.NearbyParentFragment;
import fr.free.nrw.commons.review.ReviewImageFragment;
import fr.free.nrw.commons.settings.SettingsFragment;
+import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
+import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
+import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
@@ -71,4 +74,12 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract ReviewImageFragment bindReviewOutOfContextFragment();
+ @ContributesAndroidInjector
+ abstract UploadMediaDetailFragment bindUploadMediaDetailFragment();
+
+ @ContributesAndroidInjector
+ abstract UploadCategoriesFragment bindUploadCategoriesFragment();
+
+ @ContributesAndroidInjector
+ abstract MediaLicenseFragment bindMediaLicenseFragment();
}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java
index 50eb1af56..6ca0a13af 100644
--- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java
@@ -16,10 +16,6 @@ import butterknife.ButterKnife;
import com.google.android.material.tabs.TabLayout;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxSearchView;
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import com.jakewharton.rxbinding2.view.RxView;
-import com.jakewharton.rxbinding2.widget.RxSearchView;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
@@ -33,13 +29,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import io.reactivex.disposables.Disposable;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
/**
* Represents search screen of this app
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 72789e1fc..18349f525 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
@@ -44,6 +44,7 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
+import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteHelper;
@@ -56,9 +57,6 @@ import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
-import org.apache.commons.lang3.StringUtils;
-import org.wikipedia.util.DateUtil;
-import org.wikipedia.util.StringUtil;
import timber.log.Timber;
import static android.view.View.GONE;
@@ -367,6 +365,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@OnClick(R.id.nominateDeletion)
public void onDeleteButtonClicked(){
+ if(AccountUtil.getUserName(getContext()).equals(media.getCreator())){
final ArrayAdapter languageAdapter = new ArrayAdapter<>(getActivity(),
R.layout.simple_spinner_dropdown_list, reasonList);
final Spinner spinner = new Spinner(getActivity());
@@ -384,19 +383,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if(isDeleted) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
+ }
//Reviewer correct me if i have misunderstood something over here
//But how does this if (delete.getVisibility() == View.VISIBLE) {
// enableDeleteButton(true); makes sense ?
+ else{
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
- alert.setMessage("Why should this fileckathon-2018 be deleted?");
+ alert.setMessage(getString(R.string.dialog_box_text_nomination,media.getDisplayTitle()));
final EditText input = new EditText(getActivity());
alert.setView(input);
input.requestFocus();
alert.setPositiveButton(R.string.ok, (dialog1, whichButton) -> {
String reason = input.getText().toString();
-
- deleteHelper.makeDeletion(getContext(), media, reason);
- enableDeleteButton(false);
+ onDeleteClickeddialogtext(reason);
});
alert.setNegativeButton(R.string.cancel, (dialog12, whichButton) -> {
});
@@ -427,6 +426,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
d.show();
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
+ }
@SuppressLint("CheckResult")
private void onDeleteClicked(Spinner spinner) {
@@ -445,6 +445,22 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
+ @SuppressLint("CheckResult")
+ private void onDeleteClickeddialogtext(String reason) {
+ Single resultSingletext = reasonBuilder.getReason(media, reason)
+ .flatMap(reasonString -> deleteHelper.makeDeletion(getContext(), media, reason));
+ compositeDisposable.add(resultSingletext
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(s -> {
+ if (getActivity() != null) {
+ isDeleted = true;
+ enableDeleteButton(false);
+ }
+ }));
+
+ }
+
@OnClick(R.id.seeMore)
public void onSeeMoreClicked(){
if (nominatedForDeletion.getVisibility() == VISIBLE && getActivity() != null) {
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
index 85076cb69..8f5b4213f 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
@@ -410,7 +410,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
*/
@Nullable
@Override
- public String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException {
+ public String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException {
Timber.d("Filename is %s", value);
CustomApiResult result = wikidataApi.action("wbcreateclaim")
.param("entity", entityId)
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
index 2d39740d5..e38c4dc0f 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
@@ -59,7 +59,7 @@ public interface MediaWikiApi {
String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
@Nullable
- String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException;
+ String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException;
@Nullable
boolean addWikidataEditTag(String revisionId) throws IOException;
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java
index 2b4080dce..f34a5f2cf 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java
@@ -535,8 +535,8 @@ public class NearbyMapFragment extends DaggerFragment {
.compassGravity(Gravity.BOTTOM | Gravity.LEFT)
.compassMargins(new int[]{12, 0, 0, 24})
.styleUrl(isDarkTheme ? Style.DARK : Style.OUTDOORS)
- .logoEnabled(false)
- .attributionEnabled(false)
+ .logoEnabled(true)
+ .attributionEnabled(true)
.camera(new CameraPosition.Builder()
.target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude()))
.zoom(ZOOM_LEVEL)
diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadLocalDataSource.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadLocalDataSource.java
new file mode 100644
index 000000000..3f4a58bd3
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadLocalDataSource.java
@@ -0,0 +1,150 @@
+package fr.free.nrw.commons.repository;
+
+import androidx.annotation.Nullable;
+import fr.free.nrw.commons.kvstore.JsonKvStore;
+import fr.free.nrw.commons.upload.UploadModel;
+import fr.free.nrw.commons.upload.UploadModel.UploadItem;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+/**
+ * The Local Data Source for UploadRepository, fetches and returns data from local db/shared prefernces
+ */
+
+@Singleton
+public class UploadLocalDataSource {
+
+ private final UploadModel uploadModel;
+ private JsonKvStore defaultKVStore;
+
+ @Inject
+ public UploadLocalDataSource(
+ @Named("default_preferences") JsonKvStore defaultKVStore,
+ UploadModel uploadModel) {
+ this.defaultKVStore = defaultKVStore;
+ this.uploadModel = uploadModel;
+ }
+
+
+ /**
+ * Fetches and returns the string list of valid licenses
+ *
+ * @return
+ */
+ public List getLicenses() {
+ return uploadModel.getLicenses();
+ }
+
+ /**
+ * Returns the number of Upload Items
+ *
+ * @return
+ */
+ public int getCount() {
+ return uploadModel.getCount();
+ }
+
+ /**
+ * Fetches and return the selected license for the current upload
+ *
+ * @return
+ */
+ public String getSelectedLicense() {
+ return uploadModel.getSelectedLicense();
+ }
+
+ /**
+ * Set selected license for the current upload
+ *
+ * @param licenseName
+ */
+ public void setSelectedLicense(String licenseName) {
+ uploadModel.setSelectedLicense(licenseName);
+ }
+
+ /**
+ * Updates the current upload item
+ *
+ * @param index
+ * @param uploadItem
+ */
+ public void updateUploadItem(int index, UploadItem uploadItem) {
+ uploadModel.updateUploadItem(index, uploadItem);
+ }
+
+ /**
+ * upload is halted, cleanup the acquired resources
+ */
+ public void cleanUp() {
+ uploadModel.cleanUp();
+ }
+
+ /**
+ * Deletes the upload item at the current index
+ *
+ * @param filePath
+ */
+ public void deletePicture(String filePath) {
+ uploadModel.deletePicture(filePath);
+ }
+
+ /**
+ * Fethces and returns the previous upload item, if any, returns null otherwise
+ *
+ * @param index
+ * @return
+ */
+ @Nullable
+ public UploadItem getPreviousUploadItem(int index) {
+ if (index - 1 >= 0) {
+ return uploadModel.getItems().get(index - 1);
+ }
+ return null; //There is no previous item to copy details
+ }
+
+ /**
+ * saves boolean value in default store
+ *
+ * @param key
+ * @param value
+ */
+ public void saveValue(String key, boolean value) {
+ defaultKVStore.putBoolean(key, value);
+ }
+
+ /**
+ * saves string value in default store
+ *
+ * @param key
+ * @param value
+ */
+ public void saveValue(String key, String value) {
+ defaultKVStore.putString(key, value);
+ }
+
+ /**
+ * Fetches and returns string value from the default store
+ *
+ * @param key
+ * @param defaultValue
+ * @return
+ */
+ public String getValue(String key, String defaultValue) {
+ return defaultKVStore.getString(key, defaultValue);
+ }
+
+ /**
+ * Fetches and returns boolean value from the default store
+ *
+ * @param key
+ * @param defaultValue
+ * @return
+ */
+ public boolean getValue(String key, boolean defaultValue) {
+ return defaultKVStore.getBoolean(key, defaultValue);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java
new file mode 100644
index 000000000..938b6f30d
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java
@@ -0,0 +1,179 @@
+package fr.free.nrw.commons.repository;
+
+import fr.free.nrw.commons.category.CategoriesModel;
+import fr.free.nrw.commons.category.CategoryItem;
+import fr.free.nrw.commons.contributions.Contribution;
+import fr.free.nrw.commons.filepicker.UploadableFile;
+import fr.free.nrw.commons.nearby.Place;
+import fr.free.nrw.commons.upload.SimilarImageInterface;
+import fr.free.nrw.commons.upload.UploadController;
+import fr.free.nrw.commons.upload.UploadModel;
+import fr.free.nrw.commons.upload.UploadModel.UploadItem;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+
+import java.util.Comparator;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * This class would act as the data source for remote operations for UploadActivity
+ */
+@Singleton
+public class UploadRemoteDataSource {
+
+ private UploadModel uploadModel;
+ private UploadController uploadController;
+ private CategoriesModel categoriesModel;
+
+ @Inject
+ public UploadRemoteDataSource(UploadModel uploadModel, UploadController uploadController,
+ CategoriesModel categoriesModel) {
+ this.uploadModel = uploadModel;
+ this.uploadController = uploadController;
+ this.categoriesModel = categoriesModel;
+ }
+
+ /**
+ * asks the UploadModel to build the contributions
+ *
+ * @return
+ */
+ public Observable buildContributions() {
+ return uploadModel.buildContributions();
+ }
+
+ /**
+ * asks the UploadService to star the uplaod for
+ *
+ * @param contribution
+ */
+ public void startUpload(Contribution contribution) {
+ uploadController.startUpload(contribution);
+ }
+
+ /**
+ * returns the list of UploadItem from the UploadModel
+ *
+ * @return
+ */
+ public List getUploads() {
+ return uploadModel.getUploads();
+ }
+
+ /**
+ * Prepare the UploadService for the upload
+ */
+ public void prepareService() {
+ uploadController.prepareService();
+ }
+
+ /**
+ * Clean up the UploadController
+ */
+ public void cleanup() {
+ uploadController.cleanup();
+ }
+
+ /**
+ * Clean up the selected categories
+ */
+ public void clearSelectedCategories(){
+ //This needs further refactoring, this should not be here, right now the structure wont suppoort rhis
+ categoriesModel.cleanUp();
+ }
+
+ /**
+ * returnt the list of selected categories
+ *
+ * @return
+ */
+ public List getSelectedCategories() {
+ return categoriesModel.getSelectedCategories();
+ }
+
+ /**
+ * all categories from MWApi
+ *
+ * @param query
+ * @param imageTitleList
+ * @return
+ */
+ public Observable searchAll(String query, List imageTitleList) {
+ return categoriesModel.searchAll(query, imageTitleList);
+ }
+
+ /**
+ * returns the string list of categories
+ *
+ * @return
+ */
+ public List getCategoryStringList() {
+ return categoriesModel.getCategoryStringList();
+ }
+
+ /**
+ * sets the selected categories in the UploadModel
+ *
+ * @param categoryStringList
+ */
+ public void setSelectedCategories(List categoryStringList) {
+ uploadModel.setSelectedCategories(categoryStringList);
+ }
+
+ /**
+ * handles category selection/unselection
+ *
+ * @param categoryItem
+ */
+ public void onCategoryClicked(CategoryItem categoryItem) {
+ categoriesModel.onCategoryItemClicked(categoryItem);
+ }
+
+ /**
+ * returns category sorted based on similarity with query
+ *
+ * @param query
+ * @return
+ */
+ public Comparator sortBySimilarity(String query) {
+ return categoriesModel.sortBySimilarity(query);
+ }
+
+ /**
+ * prunes the category list for irrelevant categories see #750
+ *
+ * @param name
+ * @return
+ */
+ public boolean containsYear(String name) {
+ return categoriesModel.containsYear(name);
+ }
+
+ /**
+ * pre process the UploadableFile
+ *
+ * @param uploadableFile
+ * @param place
+ * @param source
+ * @param similarImageInterface
+ * @return
+ */
+ public Observable preProcessImage(UploadableFile uploadableFile, Place place,
+ String source, SimilarImageInterface similarImageInterface) {
+ return uploadModel.preProcessImage(uploadableFile, place, source, similarImageInterface);
+ }
+
+ /**
+ * ask the UplaodModel for the image quality of the UploadItem
+ *
+ * @param uploadItem
+ * @param shouldValidateTitle
+ * @return
+ */
+ public Single getImageQuality(UploadItem uploadItem, boolean shouldValidateTitle) {
+ return uploadModel.getImageQuality(uploadItem, shouldValidateTitle);
+ }
+}
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
new file mode 100644
index 000000000..dbd0f6134
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java
@@ -0,0 +1,265 @@
+package fr.free.nrw.commons.repository;
+
+import fr.free.nrw.commons.category.CategoryItem;
+import fr.free.nrw.commons.contributions.Contribution;
+import fr.free.nrw.commons.filepicker.UploadableFile;
+import fr.free.nrw.commons.nearby.Place;
+import fr.free.nrw.commons.upload.SimilarImageInterface;
+import fr.free.nrw.commons.upload.UploadModel.UploadItem;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+
+import java.util.Comparator;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * The repository class for UploadActivity
+ */
+@Singleton
+public class UploadRepository {
+
+ private UploadLocalDataSource localDataSource;
+ private UploadRemoteDataSource remoteDataSource;
+
+ @Inject
+ public UploadRepository(UploadLocalDataSource localDataSource,
+ UploadRemoteDataSource remoteDataSource) {
+ this.localDataSource = localDataSource;
+ this.remoteDataSource = remoteDataSource;
+ }
+
+ /**
+ * asks the RemoteDataSource to build contributions
+ *
+ * @return
+ */
+ public Observable buildContributions() {
+ return remoteDataSource.buildContributions();
+ }
+
+ /**
+ * asks the RemoteDataSource to start upload for the contribution
+ *
+ * @param contribution
+ */
+ public void startUpload(Contribution contribution) {
+ remoteDataSource.startUpload(contribution);
+ }
+
+ /**
+ * Fetches and returns all the Upload Items
+ *
+ * @return
+ */
+ public List getUploads() {
+ return remoteDataSource.getUploads();
+ }
+
+ /**
+ * asks the RemoteDataSource to prepare the Upload Service
+ */
+ public void prepareService() {
+ remoteDataSource.prepareService();
+ }
+
+ /**
+ *Prepare for a fresh upload
+ */
+ public void cleanup() {
+ localDataSource.cleanUp();
+ remoteDataSource.clearSelectedCategories();
+ }
+
+ /**
+ * Fetches and returns the selected categories for the current upload
+ *
+ * @return
+ */
+ public List getSelectedCategories() {
+ return remoteDataSource.getSelectedCategories();
+ }
+
+ /**
+ * all categories from MWApi
+ *
+ * @param query
+ * @param imageTitleList
+ * @return
+ */
+ public Observable searchAll(String query, List imageTitleList) {
+ return remoteDataSource.searchAll(query, imageTitleList);
+ }
+
+ /**
+ * returns the string list of categories
+ *
+ * @return
+ */
+
+ public List getCategoryStringList() {
+ return remoteDataSource.getCategoryStringList();
+ }
+
+ /**
+ * sets the list of selected categories for the current upload
+ *
+ * @param categoryStringList
+ */
+ public void setSelectedCategories(List categoryStringList) {
+ remoteDataSource.setSelectedCategories(categoryStringList);
+ }
+
+ /**
+ * handles the category selection/deselection
+ *
+ * @param categoryItem
+ */
+ public void onCategoryClicked(CategoryItem categoryItem) {
+ remoteDataSource.onCategoryClicked(categoryItem);
+ }
+
+ /**
+ * returns category sorted based on similarity with query
+ *
+ * @param query
+ * @return
+ */
+ public Comparator super CategoryItem> sortBySimilarity(String query) {
+ return remoteDataSource.sortBySimilarity(query);
+ }
+
+ /**
+ * prunes the category list for irrelevant categories see #750
+ *
+ * @param name
+ * @return
+ */
+ public boolean containsYear(String name) {
+ return remoteDataSource.containsYear(name);
+ }
+
+ /**
+ * retursn the string list of available license from the LocalDataSource
+ *
+ * @return
+ */
+ public List getLicenses() {
+ return localDataSource.getLicenses();
+ }
+
+ /**
+ * returns the selected license for the current upload
+ *
+ * @return
+ */
+ public String getSelectedLicense() {
+ return localDataSource.getSelectedLicense();
+ }
+
+ /**
+ * returns the number of Upload Items
+ *
+ * @return
+ */
+ public int getCount() {
+ return localDataSource.getCount();
+ }
+
+ /**
+ * ask the RemoteDataSource to pre process the image
+ *
+ * @param uploadableFile
+ * @param place
+ * @param source
+ * @param similarImageInterface
+ * @return
+ */
+ public Observable preProcessImage(UploadableFile uploadableFile, Place place,
+ String source, SimilarImageInterface similarImageInterface) {
+ return remoteDataSource
+ .preProcessImage(uploadableFile, place, source, similarImageInterface);
+ }
+
+ /**
+ * query the RemoteDataSource for image quality
+ *
+ * @param uploadItem
+ * @param shouldValidateTitle
+ * @return
+ */
+ public Single getImageQuality(UploadItem uploadItem, boolean shouldValidateTitle) {
+ return remoteDataSource.getImageQuality(uploadItem, shouldValidateTitle);
+ }
+
+ /**
+ * asks the LocalDataSource to update the Upload Item
+ *
+ * @param index
+ * @param uploadItem
+ */
+ public void updateUploadItem(int index, UploadItem uploadItem) {
+ localDataSource.updateUploadItem(index, uploadItem);
+ }
+
+ /**
+ * asks the LocalDataSource to delete the file with the given file path
+ *
+ * @param filePath
+ */
+ public void deletePicture(String filePath) {
+ localDataSource.deletePicture(filePath);
+ }
+
+ /**
+ * fetches and returns the previous upload item
+ *
+ * @param index
+ * @return
+ */
+ public UploadItem getPreviousUploadItem(int index) {
+ return localDataSource.getPreviousUploadItem(index);
+ }
+
+ /**
+ * Save boolean value locally
+ *
+ * @param key
+ * @param value
+ */
+ public void saveValue(String key, boolean value) {
+ localDataSource.saveValue(key, value);
+ }
+
+ /**
+ * save string value locally
+ *
+ * @param key
+ * @param value
+ */
+ public void saveValue(String key, String value) {
+ localDataSource.saveValue(key, value);
+ }
+
+ /**
+ * fetch the string value for the associated key
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ public String getValue(String key, String value) {
+ return localDataSource.getValue(key, value);
+ }
+
+ /**
+ * set selected license for the current upload
+ *
+ * @param licenseName
+ */
+ public void setSelectedLicense(String licenseName) {
+ localDataSource.setSelectedLicense(licenseName);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Description.java b/app/src/main/java/fr/free/nrw/commons/upload/Description.java
index ae18d4adb..c6f69584e 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/Description.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/Description.java
@@ -5,11 +5,12 @@ import java.util.List;
/**
* Holds a description of an item being uploaded by {@link UploadActivity}
*/
-class Description {
+public class Description {
private String languageCode;
private String descriptionText;
private int selectedLanguageIndex = -1;
+ private boolean isManuallyAdded=false;
/**
* @return The language code ie. "en" or "fr"
@@ -47,6 +48,21 @@ class Description {
this.selectedLanguageIndex = selectedLanguageIndex;
}
+ /**
+ * returns if the description was added manually (by the user, or we have added it programaticallly)
+ * @return
+ */
+ public boolean isManuallyAdded() {
+ return isManuallyAdded;
+ }
+
+ /**
+ * sets to true if the description was manually added by the user
+ * @param manuallyAdded
+ */
+ public void setManuallyAdded(boolean manuallyAdded) {
+ isManuallyAdded = manuallyAdded;
+ }
/**
* Formats the list of descriptions into the format Commons requires for uploads.
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java
index 7aca51908..a24a791bf 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java
@@ -1,22 +1,22 @@
package fr.free.nrw.commons.upload;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
+import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
-import android.widget.EditText;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.AppCompatSpinner;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
@@ -24,60 +24,35 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.AbstractTextWatcher;
import fr.free.nrw.commons.utils.BiMap;
-import fr.free.nrw.commons.utils.ViewUtil;
-import io.reactivex.subjects.BehaviorSubject;
-import io.reactivex.subjects.Subject;
import timber.log.Timber;
-class DescriptionsAdapter extends RecyclerView.Adapter {
+public class DescriptionsAdapter extends RecyclerView.Adapter {
- private Title title;
private List descriptions;
- private Context context;
private Callback callback;
- private Subject titleChangedSubject;
private BiMap selectedLanguages;
- private UploadView uploadView;
- DescriptionsAdapter(UploadView uploadView) {
- title = new Title();
+ public DescriptionsAdapter() {
descriptions = new ArrayList<>();
- titleChangedSubject = BehaviorSubject.create();
selectedLanguages = new BiMap<>();
- this.uploadView = uploadView;
}
- void setCallback(Callback callback) {
+ public void setCallback(Callback callback) {
this.callback = callback;
}
- void setItems(Title title, List descriptions) {
+ public void setItems(List descriptions) {
this.descriptions = descriptions;
- this.title = title;
selectedLanguages = new BiMap<>();
notifyDataSetChanged();
}
- @Override
- public int getItemViewType(int position) {
- if (position == 0) return 1;
- else return 2;
- }
-
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- View view;
- if (viewType == 1) {
- view = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.row_item_title, parent, false);
- } else {
- view = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.row_item_description, parent, false);
- }
- context = parent.getContext();
- return new ViewHolder(view);
+ return new ViewHolder(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.row_item_description, parent, false));
}
@Override
@@ -87,29 +62,21 @@ class DescriptionsAdapter extends RecyclerView.Adapter getDescriptions() {
+ public List getDescriptions() {
return descriptions;
}
- void addDescription(Description description) {
+ public void addDescription(Description description) {
this.descriptions.add(description);
- notifyItemInserted(descriptions.size() + 1);
- }
-
- public Title getTitle() {
- return title;
- }
-
- public void setTitle(Title title) {
- this.title = title;
- notifyItemInserted(0);
+ notifyItemInserted(descriptions.size());
}
public class ViewHolder extends RecyclerView.ViewHolder {
@@ -119,98 +86,53 @@ class DescriptionsAdapter extends RecyclerView.Adapter{
- title.setTitleText(titleText);
- titleChangedSubject.onNext(titleText);
- }));
-
- descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
- if (!hasFocus) {
- ViewUtil.hideKeyboard(v);
- }
- });
-
+ descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(),
+ null);
descItemEditText.setOnTouchListener((v, event) -> {
- // Check this is a touch up event
- if(event.getAction() != MotionEvent.ACTION_UP) return false;
-
- // Check we are tapping within 15px of the info icon
- int extraTapArea = 15;
- Drawable info = descItemEditText.getCompoundDrawables()[2];
- int infoHitboxX = descItemEditText.getWidth() - info.getBounds().width();
- if (event.getX() + extraTapArea < infoHitboxX) return false;
-
- // If the above are true, show the info dialog
- callback.showAlert(R.string.media_detail_title, R.string.title_info);
- return true;
+ //2 is for drawable right
+ float twelveDpInPixels = convertDpToPixel(12, descItemEditText.getContext());
+ if (event.getAction() == MotionEvent.ACTION_UP && descItemEditText.getCompoundDrawables()[2].getBounds().contains((int)(descItemEditText.getWidth()-(event.getX()+twelveDpInPixels)),(int)(event.getY()-twelveDpInPixels))){
+ if (getAdapterPosition() == 0) {
+ callback.showAlert(R.string.media_detail_description,
+ R.string.description_info);
+ }
+ return true;
+ }
+ return false;
});
} else {
- Description description = descriptions.get(position - 1);
- Timber.d("Description is " + description);
- if (!TextUtils.isEmpty(description.getDescriptionText())) {
- descItemEditText.setText(description.getDescriptionText());
- } else {
- descItemEditText.setText("");
- }
-
- // Show the info icon for the first description
- if (position == 1) {
- descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null);
- descItemEditText.setOnTouchListener((v, event) -> {
- // Check this is a touch up event
- if(event.getAction() != MotionEvent.ACTION_UP) return false;
-
- // Check we are tapping within 15px of the info icon
- int extraTapArea = 15;
- Drawable info = descItemEditText.getCompoundDrawables()[2];
- int infoHitboxX = descItemEditText.getWidth() - info.getBounds().width();
- if (event.getX() + extraTapArea < infoHitboxX) return false;
-
- // If the above are true, show the info dialog
- callback.showAlert(R.string.media_detail_description, R.string.description_info);
- return true;
- });
- }
-
- descItemEditText.addTextChangedListener(new AbstractTextWatcher(descriptionText->{
- descriptions.get(position - 1).setDescriptionText(descriptionText);
- }));
-
- descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
- if (!hasFocus) {
- ViewUtil.hideKeyboard(v);
- } else {
- uploadView.setTopCardState(false);
- }
- });
-
- initLanguageSpinner(position, description);
+ descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
}
+ descItemEditText.addTextChangedListener(new AbstractTextWatcher(
+ descriptionText -> descriptions.get(position)
+ .setDescriptionText(descriptionText)));
+ initLanguageSpinner(position, description);
+
+ //If the description was manually added by the user, it deserves focus, if not, let the user decide
+ if (description.isManuallyAdded()) {
+ descItemEditText.requestFocus();
+ } else {
+ descItemEditText.clearFocus();
+ }
}
/**
@@ -219,48 +141,24 @@ class DescriptionsAdapter extends RecyclerView.Adapter= 0) {
- // sets the spinner value to the index of first non-selected language
- spinnerDescriptionLanguages.setSelection(availableLangIndex);
- selectedLanguages.put(spinnerDescriptionLanguages, languagesAdapter.getLanguageCode(position));
- }
- }
- } else {
- spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex());
- selectedLanguages.put(spinnerDescriptionLanguages, description.getLanguageCode());
- }
-
- //TODO do it the butterknife way
spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView> adapterView, View view, int position,
- long l) {
+ long l) {
description.setSelectedLanguageIndex(position);
- String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter()).getLanguageCode(position);
+ String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter())
+ .getLanguageCode(position);
description.setLanguageCode(languageCode);
selectedLanguages.remove(adapterView);
selectedLanguages.put(adapterView, languageCode);
- ((SpinnerLanguagesAdapter) adapterView.getAdapter()).selectedLangCode = languageCode;
+ ((SpinnerLanguagesAdapter) adapterView
+ .getAdapter()).selectedLangCode = languageCode;
}
@Override
@@ -268,18 +166,43 @@ class DescriptionsAdapter extends RecyclerView.Adapter {
+
+ List uploadableFiles;
+ private Callback callback;
+
+ public ThumbnailsAdapter(Callback callback) {
+ this.uploadableFiles = new ArrayList<>();
+ this.callback = callback;
+ }
+
+ /**
+ * Sets the data, the media files
+ * @param uploadableFiles
+ */
+ public void setUploadableFiles(
+ List uploadableFiles) {
+ this.uploadableFiles=uploadableFiles;
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
+ return new ViewHolder(LayoutInflater.from(viewGroup.getContext())
+ .inflate(R.layout.item_upload_thumbnail, viewGroup, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
+ viewHolder.bind(position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return uploadableFiles.size();
+ }
+
+ public class ViewHolder extends RecyclerView.ViewHolder {
+
+ @BindView(R.id.rl_container)
+ RelativeLayout rlContainer;
+ @BindView(R.id.iv_thumbnail)
+ SimpleDraweeView background;
+ @BindView(R.id.iv_error)
+ ImageView ivError;
+
+ public ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ ButterKnife.bind(this, itemView);
+ }
+
+ /**
+ * Binds a row item to the ViewHolder
+ * @param position
+ */
+ public void bind(int position) {
+ UploadableFile uploadableFile = uploadableFiles.get(position);
+ Uri uri = uploadableFile.getMediaUri();
+ background.setImageURI(Uri.fromFile(new File(String.valueOf(uri))));
+
+ if (position == callback.getCurrentSelectedFilePosition()) {
+ rlContainer.setEnabled(true);
+ rlContainer.setClickable(true);
+ rlContainer.setAlpha(1.0f);
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ rlContainer.setElevation(10);
+ }
+ } else {
+ rlContainer.setEnabled(false);
+ rlContainer.setClickable(false);
+ rlContainer.setAlpha(0.5f);
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ rlContainer.setElevation(0);
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback used to get the current selected file position
+ */
+ interface Callback {
+
+ int getCurrentSelectedFilePosition();
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Title.java b/app/src/main/java/fr/free/nrw/commons/upload/Title.java
index bc2d55640..380b2c1de 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/Title.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/Title.java
@@ -31,4 +31,8 @@ public class Title{
public boolean isEmpty() {
return titleText==null || titleText.isEmpty();
}
+
+ public String getTitleText() {
+ return titleText;
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
index 347d6e71c..8881151e1 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
@@ -1,162 +1,115 @@
package fr.free.nrw.commons.upload;
+import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
+import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
+import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
+
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.os.Bundle;
-import com.google.android.material.textfield.TextInputLayout;
import androidx.appcompat.app.AlertDialog;
import androidx.cardview.widget.CardView;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
-import android.text.style.ClickableSpan;
-import android.text.style.URLSpan;
-import android.view.MotionEvent;
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.ProgressBar;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
import android.widget.RelativeLayout;
-import android.widget.Spinner;
import android.widget.TextView;
-import android.widget.Toast;
-import android.widget.ViewFlipper;
-
-import com.github.chrisbanes.photoview.PhotoView;
-import com.jakewharton.rxbinding2.view.RxView;
-import com.jakewharton.rxbinding2.widget.RxTextView;
-import com.pedrogomez.renderers.RVRendererAdapter;
-
import java.util.ArrayList;
-import java.util.LinkedList;
import java.util.List;
-import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
+import butterknife.OnClick;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategoriesModel;
-import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionController;
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.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.theme.BaseActivity;
-import fr.free.nrw.commons.ui.widget.HtmlTextView;
-import fr.free.nrw.commons.utils.DialogUtil;
-import fr.free.nrw.commons.utils.NetworkUtils;
+import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
+import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
+import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
+import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
-import io.reactivex.Observable;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.schedulers.Schedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import java.util.Collections;
import timber.log.Timber;
-import static fr.free.nrw.commons.contributions.Contribution.SOURCE_EXTERNAL;
-import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
-import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
-import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
-
-public class UploadActivity extends BaseActivity implements UploadView, SimilarImageInterface {
- @Inject MediaWikiApi mwApi;
+public class UploadActivity extends BaseActivity implements UploadContract.View ,UploadBaseFragment.Callback{
@Inject
ContributionController contributionController;
@Inject @Named("default_preferences") JsonKvStore directKvStore;
- @Inject UploadPresenter presenter;
+ @Inject UploadContract.UserActionListener presenter;
@Inject CategoriesModel categoriesModel;
@Inject SessionManager sessionManager;
- // Main GUI
- @BindView(R.id.backgroundImage) PhotoView background;
- @BindView(R.id.upload_root_layout)
- RelativeLayout rootLayout;
- @BindView(R.id.view_flipper) ViewFlipper viewFlipper;
+ @BindView(R.id.cv_container_top_card)
+ CardView cvContainerTopCard;
- // Top Card
- @BindView(R.id.top_card) CardView topCard;
- @BindView(R.id.top_card_expand_button) ImageView topCardExpandButton;
- @BindView(R.id.top_card_title) TextView topCardTitle;
- @BindView(R.id.top_card_thumbnails) RecyclerView topCardThumbnails;
+ @BindView(R.id.ll_container_top_card)
+ LinearLayout llContainerTopCard;
- // Bottom Card
- @BindView(R.id.bottom_card) CardView bottomCard;
- @BindView(R.id.bottom_card_expand_button) ImageView bottomCardExpandButton;
- @BindView(R.id.bottom_card_title) TextView bottomCardTitle;
- @BindView(R.id.bottom_card_subtitle) TextView bottomCardSubtitle;
- @BindView(R.id.bottom_card_next) Button next;
- @BindView(R.id.bottom_card_previous) Button previous;
- @BindView(R.id.bottom_card_add_desc) Button bottomCardAddDescription;
- @BindView(R.id.prev_title_desc) Button prevTitleDecs;
- @BindView(R.id.categories_subtitle) TextView categoriesSubtitle;
- @BindView(R.id.license_subtitle) TextView licenseSubtitle;
- @BindView(R.id.please_wait_text_view) TextView pleaseWaitTextView;
+ @BindView(R.id.rl_container_title)
+ RelativeLayout rlContainerTitle;
+ @BindView(R.id.tv_top_card_title)
+ TextView tvTopCardTitle;
- @BindView(R.id.right_card_map_button) View rightCardMapButton;
+ @BindView(R.id.ib_toggle_top_card)
+ ImageButton ibToggleTopCard;
- // Category Search
- @BindView(R.id.categories_title) TextView categoryTitle;
- @BindView(R.id.category_next) Button categoryNext;
- @BindView(R.id.category_previous) Button categoryPrevious;
- @BindView(R.id.categoriesSearchInProgress) ProgressBar categoriesSearchInProgress;
- @BindView(R.id.category_search) EditText categoriesSearch;
- @BindView(R.id.category_search_container) TextInputLayout categoriesSearchContainer;
- @BindView(R.id.categories) RecyclerView categoriesList;
- @BindView(R.id.category_search_layout)
- FrameLayout categoryFrameLayout;
+ @BindView(R.id.rv_thumbnails)
+ RecyclerView rvThumbnails;
- // Final Submission
- @BindView(R.id.license_title) TextView licenseTitle;
- @BindView(R.id.share_license_summary) HtmlTextView licenseSummary;
- @BindView(R.id.license_list) Spinner licenseSpinner;
- @BindView(R.id.submit) Button submit;
- @BindView(R.id.license_previous) Button licensePrevious;
- @BindView(R.id.rv_descriptions) RecyclerView rvDescriptions;
+ @BindView(R.id.vp_upload)
+ ViewPager vpUpload;
- private DescriptionsAdapter descriptionsAdapter;
- private RVRendererAdapter categoriesAdapter;
+ private boolean isTitleExpanded=true;
+
+ private CompositeDisposable compositeDisposable;
private ProgressDialog progressDialog;
- private boolean multipleUpload = false, flagForSubmit = false;
+ private UploadImageAdapter uploadImagesAdapter;
+ private List fragments;
+ private UploadCategoriesFragment uploadCategoriesFragment;
+ private MediaLicenseFragment mediaLicenseFragment;
+ private ThumbnailsAdapter thumbnailsAdapter;
+ private String source;
+ private Place place;
+ private List uploadableFiles= Collections.emptyList();
+ private int currentSelectedPosition=0;
+
@SuppressLint("CheckResult")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_upload);
+
ButterKnife.bind(this);
-
- configureLayout();
- configureTopCard();
- configureBottomCard();
- initRecyclerView();
- configureRightCard();
- configureNavigationButtons();
- configureCategories();
- configureLicenses();
-
- presenter.init();
+ compositeDisposable = new CompositeDisposable();
+ init();
PermissionUtils.checkPermissionsAndPerformAction(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
@@ -165,283 +118,150 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
R.string.write_storage_permission_rationale_for_image_share);
}
- @Override
- public boolean checkIfLoggedIn() {
- if (!sessionManager.isUserLoggedIn()) {
- Timber.d("Current account is null");
- ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in));
- Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class);
- startActivity(loginIntent);
- return false;
- }
- return true;
+ private void init() {
+ initProgressDialog();
+ initViewPager();
+ initThumbnailsRecyclerView();
+ //And init other things you need to
+ }
+
+ private void initProgressDialog() {
+ progressDialog = new ProgressDialog(this);
+ progressDialog.setMessage(getString(R.string.please_wait));
+ }
+
+ private void initThumbnailsRecyclerView() {
+ rvThumbnails.setLayoutManager(new LinearLayoutManager(this,
+ LinearLayoutManager.HORIZONTAL, false));
+ thumbnailsAdapter=new ThumbnailsAdapter(() -> currentSelectedPosition);
+ rvThumbnails.setAdapter(thumbnailsAdapter);
+
+ }
+
+ private void initViewPager() {
+ uploadImagesAdapter=new UploadImageAdapter(getSupportFragmentManager());
+ vpUpload.setAdapter(uploadImagesAdapter);
+ vpUpload.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+ @Override
+ public void onPageScrolled(int position, float positionOffset,
+ int positionOffsetPixels) {
+
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ currentSelectedPosition=position;
+ if (position >= uploadableFiles.size()) {
+ cvContainerTopCard.setVisibility(View.GONE);
+ } else {
+ thumbnailsAdapter.notifyDataSetChanged();
+ cvContainerTopCard.setVisibility(View.VISIBLE);
+ }
+
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+
+ }
+ });
}
@Override
- protected void onDestroy() {
- presenter.cleanup();
- super.onDestroy();
+ public boolean isLoggedIn() {
+ return sessionManager.isUserLoggedIn();
}
@Override
protected void onResume() {
super.onResume();
- checkIfLoggedIn();
-
+ presenter.onAttachView(this);
+ if (!isLoggedIn()) {
+ askUserToLogIn();
+ }
checkStoragePermissions();
- compositeDisposable.add(
- RxTextView.textChanges(categoriesSearch)
- .doOnEach(v -> categoriesSearchContainer.setError(null))
- .takeUntil(RxView.detaches(categoriesSearch))
- .debounce(500, TimeUnit.MILLISECONDS)
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(filter -> updateCategoryList(filter.toString()), Timber::e)
- );
}
private void checkStoragePermissions() {
PermissionUtils.checkPermissionsAndPerformAction(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
- () -> presenter.addView(this),
+ () -> {
+ //TODO handle this
+ },
R.string.storage_permission_title,
R.string.write_storage_permission_rationale_for_image_share);
}
- @Override
- protected void onPause() {
- presenter.removeView();
- super.onPause();
- }
@Override
- public void updateThumbnails(List uploads) {
- int uploadCount = uploads.size();
- topCardThumbnails.setAdapter(new UploadThumbnailsAdapterFactory(presenter::thumbnailClicked).create(uploads));
- topCardTitle.setText(getResources().getQuantityString(R.plurals.upload_count_title, uploadCount, uploadCount));
- }
-
- @Override
- public void updateRightCardContent(boolean gpsPresent) {
- if (gpsPresent) {
- rightCardMapButton.setVisibility(View.VISIBLE);
- }
- else {
- rightCardMapButton.setVisibility(View.GONE);
- }
- //The card should be disabled if it has no buttons.
- setRightCardVisibility(gpsPresent);
- }
-
- @Override
- public void updateBottomCardContent(int currentStep,
- int stepCount,
- UploadModel.UploadItem uploadItem,
- boolean isShowingItem) {
- boolean saveForPrevImage = false;
- int singleUploadStepCount = 3;
-
- String cardTitle = getResources().getString(R.string.step_count, currentStep, stepCount);
- String cardSubTitle = getResources().getString(R.string.image_in_set_label, currentStep);
- bottomCardTitle.setText(cardTitle);
- bottomCardSubtitle.setText(cardSubTitle);
- categoryTitle.setText(cardTitle);
- licenseTitle.setText(cardTitle);
- if (currentStep == stepCount) {
- dismissKeyboard();
- }
- if (stepCount > singleUploadStepCount) {
- multipleUpload = true;
- }
- if (multipleUpload && currentStep != 1) {
- saveForPrevImage = true;
- }
- configurePrevButton(saveForPrevImage);
- if(isShowingItem) {
- descriptionsAdapter.setItems(uploadItem.getTitle(), uploadItem.getDescriptions());
- rvDescriptions.setAdapter(descriptionsAdapter);
- }
- }
-
- @Override
- public void updateLicenses(List licenses, String selectedLicense) {
- ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, licenses);
- licenseSpinner.setAdapter(adapter);
-
- int position = licenses.indexOf(getString(Utils.licenseNameFor(selectedLicense)));
-
- // Check position is valid
- if (position < 0) {
- Timber.d("Invalid position: %d. Using default license", position);
- position = licenses.size() - 1;
- }
-
- Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(selectedLicense)));
- licenseSpinner.setSelection(position);
- }
-
- @SuppressLint("StringFormatInvalid")
- @Override
- public void updateLicenseSummary(String selectedLicense, int imageCount) {
- String licenseHyperLink = "" +
- getString(Utils.licenseNameFor(selectedLicense)) + "
";
- licenseSummary.setHtmlText(getResources().getQuantityString(R.plurals.share_license_summary, imageCount, licenseHyperLink));
- }
-
- @Override
- public void updateTopCardContent() {
- RecyclerView.Adapter adapter = topCardThumbnails.getAdapter();
- if (adapter != null) {
- adapter.notifyDataSetChanged();
- }
- }
-
- @Override
- public void setNextEnabled(boolean available) {
- next.setEnabled(available);
- categoryNext.setEnabled(available);
- }
-
- @Override
- public void setSubmitEnabled(boolean available) {
- submit.setEnabled(available);
- }
-
- @Override
- public void setPreviousEnabled(boolean available) {
- previous.setEnabled(available);
- categoryPrevious.setEnabled(available);
- licensePrevious.setEnabled(available);
- }
-
- @Override
- public void setTopCardState(boolean state) {
- updateCardState(state, topCardExpandButton, topCardThumbnails);
- }
-
- @Override
- public void setTopCardVisibility(boolean visible) {
- topCard.setVisibility(visible ? View.VISIBLE : View.GONE);
- }
-
- @Override
- public void setBottomCardVisibility(boolean visible) {
- bottomCard.setVisibility(visible ? View.VISIBLE : View.GONE);
- }
-
- @Override
- public void setRightCardVisibility(boolean visible) {
- rightCardMapButton.setVisibility(visible ? View.VISIBLE : View.GONE);
- }
-
- @Override
- public void setBottomCardVisibility(@UploadPage int page, int uploadCount) {
- if (page == TITLE_CARD) {
- viewFlipper.setDisplayedChild(0);
- } else if (page == CATEGORIES) {
- viewFlipper.setDisplayedChild(1);
- } else if (page == LICENSE) {
- viewFlipper.setDisplayedChild(2);
- dismissKeyboard();
- } else if (page == PLEASE_WAIT) {
- viewFlipper.setDisplayedChild(3);
- pleaseWaitTextView.setText(getResources().getQuantityText(R.plurals.receiving_shared_content, uploadCount));
- }
+ protected void onStop() {
+ super.onStop();
}
/**
- * Only show the subtitle ("For all images in set") if multiple images being uploaded
- * @param imageCount Number of images being uploaded
+ * Show/Hide the progress dialog
*/
@Override
- public void updateSubtitleVisibility(int imageCount) {
- categoriesSubtitle.setVisibility(imageCount > 1 ? View.VISIBLE : View.GONE);
- licenseSubtitle.setVisibility(imageCount > 1 ? View.VISIBLE : View.GONE);
- }
-
- @Override
- public void setBottomCardState(boolean state) {
- updateCardState(state, bottomCardExpandButton, rvDescriptions, previous, next, prevTitleDecs, bottomCardAddDescription);
- }
-
-
- @Override
- public void setBackground(Uri mediaUri) {
- background.setImageURI(mediaUri);
- }
-
-
- @Override
- public void dismissKeyboard() {
- InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
-
- // verify if the soft keyboard is open
- if (imm != null && imm.isAcceptingText() && getCurrentFocus() != null) {
- imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
+ public void showProgress(boolean shouldShow) {
+ if (shouldShow) {
+ if (!progressDialog.isShowing()) {
+ progressDialog.show();
+ }
+ } else {
+ if (progressDialog != null && !isFinishing()) {
+ progressDialog.dismiss();
+ }
}
}
@Override
- public void showBadPicturePopup(String errorMessage) {
- DialogUtil.showAlertDialog(this,
- getString(R.string.warning),
- errorMessage,
- () -> presenter.deletePicture(),
- () -> presenter.keepPicture());
+ public int getIndexInViewFlipper(UploadBaseFragment fragment) {
+ return fragments.indexOf(fragment);
}
@Override
- public void showDuplicatePicturePopup() {
- DialogUtil.showAlertDialog(this,
- getString(R.string.warning),
- String.format(getString(R.string.upload_title_duplicate), presenter.getCurrentImageFileName()),
- null,
- () -> {
- presenter.keepPicture();
- presenter.handleNext(descriptionsAdapter.getTitle(), getDescriptions());
- });
- }
-
- public void showNoCategorySelectedWarning() {
- DialogUtil.showAlertDialog(this,
- getString(R.string.no_categories_selected),
- getString(R.string.no_categories_selected_warning_desc),
- getString(R.string.no_go_back),
- getString(R.string.yes_submit),
- null,
- () -> presenter.handleCategoryNext(categoriesModel, true));
+ public int getTotalNumberOfSteps() {
+ return fragments.size();
}
@Override
- public void showProgressDialog() {
- if (progressDialog == null) {
- progressDialog = new ProgressDialog(this);
- }
- progressDialog.setMessage(getString(R.string.please_wait));
- progressDialog.show();
+ public void showMessage(int messageResourceId) {
+ ViewUtil.showLongToast(this, messageResourceId);
}
@Override
- public void hideProgressDialog() {
- if (progressDialog != null && !isFinishing()) {
- progressDialog.dismiss();
- }
+ public List getUploadableFiles() {
+ return uploadableFiles;
}
@Override
- public void launchMapActivity(LatLng decCoords) {
- Utils.handleGeoCoordinates(this, decCoords);
+ public void showHideTopCard(boolean shouldShow) {
+ llContainerTopCard.setVisibility(shouldShow?View.VISIBLE:View.GONE);
}
@Override
- public void showErrorMessage(int resourceId) {
- ViewUtil.showShortToast(this, resourceId);
+ public void onUploadMediaDeleted(int index) {
+ fragments.remove(index);//Remove the corresponding fragment
+ uploadableFiles.remove(index);//Remove the files from the list
+ thumbnailsAdapter.notifyItemRemoved(index); //Notify the thumbnails adapter
+ uploadImagesAdapter.notifyDataSetChanged(); //Notify the ViewPager
}
@Override
- public void initDefaultCategories() {
- updateCategoryList("");
+ public void updateTopCardTitle() {
+ tvTopCardTitle.setText(getResources()
+ .getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size()));
}
+ @Override
+ public void askUserToLogIn() {
+ Timber.d("current session is null, asking user to login");
+ ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in));
+ Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class);
+ startActivity(loginIntent);
+ }
+
+
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@@ -450,179 +270,6 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
}
}
- private void configureLicenses() {
- licenseSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView> parent, View view, int position, long id) {
- String licenseName = parent.getItemAtPosition(position).toString();
- presenter.selectLicense(licenseName);
- }
-
- @Override
- public void onNothingSelected(AdapterView> parent) {
- presenter.selectLicense(null);
- }
- });
- }
-
- private void configureLayout() {
- background.setScaleType(ImageView.ScaleType.CENTER_CROP);
- background.setOnScaleChangeListener((scaleFactor, x, y) -> presenter.closeAllCards());
- }
-
- private void configureTopCard() {
- topCardExpandButton.setOnClickListener(v -> presenter.toggleTopCardState());
- topCardThumbnails.setLayoutManager(new LinearLayoutManager(this,
- LinearLayoutManager.HORIZONTAL, false));
- }
-
- private void configureBottomCard() {
- boolean flagVal = directKvStore.getBoolean("flagForSubmit");
- if(flagVal){
- prevTitleDecs.setVisibility(View.VISIBLE);
- }
- else {
- prevTitleDecs.setVisibility(View.INVISIBLE);
- }
- bottomCardExpandButton.setOnClickListener(v -> presenter.toggleBottomCardState());
- bottomCard.setOnClickListener(v -> presenter.toggleBottomCardState());
- bottomCardAddDescription.setOnClickListener(v -> addNewDescription());
- }
-
- private void addNewDescription() {
- descriptionsAdapter.addDescription(new Description());
- rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1);
- }
-
- private void configureRightCard() {
- rightCardMapButton.setOnClickListener(v -> presenter.openCoordinateMap());
- }
-
- @SuppressLint("ClickableViewAccessibility")
- public void configurePrevButton(Boolean saveForPrevImage){
- prevTitleDecs.setCompoundDrawablesWithIntrinsicBounds(null, null, getResources().getDrawable(R.drawable.mapbox_info_icon_default), null);
-
- String name = "prev_";
- if (saveForPrevImage) {
- name = name + "image_";
- } else {
- name = name + "upload_";
- }
- String title = directKvStore.getString(name + "title");
- Title t = new Title();
- t.setTitleText(title);
-
- List finalDesc = new LinkedList<>();
- int descCount = directKvStore.getInt(name + "descCount");
- for (int i = 0; i < descCount; i++) {
- Description description= new Description();
- String desc = directKvStore.getString(name + "description_<" + i + ">");
- description.setDescriptionText(desc);
- finalDesc.add(description);
- int position = directKvStore.getInt(name + "spinnerPosition_<" + i + ">");
- description.setSelectedLanguageIndex(position);
- }
- prevTitleDecs.setOnTouchListener((v, event) -> {
- // Check this is a touch up event
- if(event.getAction() != MotionEvent.ACTION_UP) return false;
- // Check we are tapping within 15px of the info icon
- int extraTapArea = 15;
- Drawable info = prevTitleDecs.getCompoundDrawables()[2];
- int infoHintbox = prevTitleDecs.getWidth() - info.getBounds().width();
- if (event.getX() + extraTapArea < infoHintbox) return false;
-
- DialogUtil.showAlertDialog(this, null, getString(R.string.previous_button_tooltip_message), "okay", null, null, null);
-
- return true;
- });
- prevTitleDecs.setOnClickListener((View v) -> {
- descriptionsAdapter.setItems(t, finalDesc);
- rvDescriptions.setAdapter(descriptionsAdapter);
- });
- }
-
- private void configureNavigationButtons() {
- // Navigation next / previous for each image as we're collecting title + description
- next.setOnClickListener(v -> {
- if (!NetworkUtils.isInternetConnectionEstablished(this)) {
- ViewUtil.showShortSnackbar(rootLayout, R.string.no_internet);
- return;
- }
- setTitleAndDescriptions();
- if (multipleUpload) {
- savePrevTitleDesc("prev_image_");
- }
- presenter.handleNext(descriptionsAdapter.getTitle(),
- descriptionsAdapter.getDescriptions());
- });
- previous.setOnClickListener(v -> presenter.handlePrevious());
-
- // Next / previous for the category selection currentPage
- categoryNext.setOnClickListener(v -> presenter.handleCategoryNext(categoriesModel, false));
- categoryPrevious.setOnClickListener(v -> presenter.handlePrevious());
-
- // Finally, the previous / submit buttons on the final currentPage of the wizard
- licensePrevious.setOnClickListener(v -> presenter.handlePrevious());
- submit.setOnClickListener(v -> {
- flagForSubmit = true;
- directKvStore.putBoolean("flagForSubmit", flagForSubmit);
- savePrevTitleDesc("prev_upload_");
- Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG).show();
- presenter.handleSubmit(categoriesModel);
- finish();
- });
-
- }
-
- private void setTitleAndDescriptions() {
- List descriptions = descriptionsAdapter.getDescriptions();
- Timber.d("Descriptions size is %d are %s", descriptions.size(), descriptions);
- }
-
- private void configureCategories() {
- categoryFrameLayout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
- categoriesAdapter = new UploadCategoriesAdapterFactory(categoriesModel).create(new ArrayList<>());
- categoriesList.setLayoutManager(new LinearLayoutManager(this));
- categoriesList.setAdapter(categoriesAdapter);
- }
-
- @SuppressLint("CheckResult")
- private void updateCategoryList(String filter) {
- List imageTitleList = presenter.getImageTitleList();
- compositeDisposable.add(Observable.fromIterable(categoriesModel.getSelectedCategories())
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .doOnSubscribe(disposable -> {
- categoriesSearchInProgress.setVisibility(View.VISIBLE);
- categoriesSearchContainer.setError(null);
- categoriesAdapter.clear();
- })
- .observeOn(Schedulers.io())
- .concatWith(
- categoriesModel.searchAll(filter, imageTitleList)
- .mergeWith(categoriesModel.searchCategories(filter, imageTitleList))
- .concatWith(TextUtils.isEmpty(filter)
- ? categoriesModel.defaultCategories(imageTitleList) : Observable.empty())
- )
- .filter(categoryItem -> !categoriesModel.containsYear(categoryItem.getName()))
- .distinct()
- .sorted(categoriesModel.sortBySimilarity(filter))
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(
- s -> categoriesAdapter.add(s),
- Timber::e,
- () -> {
- categoriesAdapter.notifyDataSetChanged();
- categoriesSearchInProgress.setVisibility(View.GONE);
-
- if (categoriesAdapter.getItemCount() == categoriesModel.selectedCategoriesCount()
- && !categoriesSearch.getText().toString().isEmpty()) {
- categoriesSearchContainer.setError("No categories found");
- }
- }
- ));
- }
-
private void receiveSharedItems() {
Intent intent = getIntent();
String action = intent.getAction();
@@ -631,21 +278,79 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
} else if (ACTION_INTERNAL_UPLOADS.equals(action)) {
receiveInternalSharedItems();
}
+
+ if (uploadableFiles == null || uploadableFiles.isEmpty()) {
+ handleNullMedia();
+ } else {
+ //Show thumbnails
+ if (uploadableFiles.size()
+ > 1) {//If there is only file, no need to show the image thumbnails
+ thumbnailsAdapter.setUploadableFiles(uploadableFiles);
+ } else {
+ llContainerTopCard.setVisibility(View.GONE);
+ }
+ tvTopCardTitle.setText(getResources()
+ .getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(),uploadableFiles.size()));
+
+ fragments = new ArrayList<>();
+ for (UploadableFile uploadableFile : uploadableFiles) {
+ UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
+ uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, source, place);
+ uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback(){
+ @Override
+ public void deletePictureAtIndex(int index) {
+ presenter.deletePictureAtIndex(index);
+ }
+
+ @Override
+ public void onNextButtonClicked(int index) {
+ UploadActivity.this.onNextButtonClicked(index);
+ }
+
+ @Override
+ public void onPreviousButtonClicked(int index) {
+ UploadActivity.this.onPreviousButtonClicked(index);
+ }
+
+ @Override
+ public void showProgress(boolean shouldShow) {
+ UploadActivity.this.showProgress(shouldShow);
+ }
+
+ @Override
+ public int getIndexInViewFlipper(UploadBaseFragment fragment) {
+ return fragments.indexOf(fragment);
+ }
+
+ @Override
+ public int getTotalNumberOfSteps() {
+ return fragments.size();
+ }
+ });
+ fragments.add(uploadMediaDetailFragment);
+ }
+
+ uploadCategoriesFragment = new UploadCategoriesFragment();
+ uploadCategoriesFragment.setCallback(this);
+
+ mediaLicenseFragment = new MediaLicenseFragment();
+ mediaLicenseFragment.setCallback(this);
+
+
+ fragments.add(uploadCategoriesFragment);
+ fragments.add(mediaLicenseFragment);
+
+ uploadImagesAdapter.setFragments(fragments);
+ vpUpload.setOffscreenPageLimit(fragments.size());
+ }
}
private void receiveExternalSharedItems() {
- List uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent());
- if (uploadableFiles.isEmpty()) {
- handleNullMedia();
- return;
- }
-
- presenter.receive(uploadableFiles, SOURCE_EXTERNAL, null);
+ uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent());
}
private void receiveInternalSharedItems() {
Intent intent = getIntent();
- String source;
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
@@ -658,17 +363,10 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
intent.getAction(),
source);
- ArrayList uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES);
+ uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES);
Timber.i("Received multiple upload %s", uploadableFiles.size());
- if (uploadableFiles.isEmpty()) {
- handleNullMedia();
- return;
- }
-
- Place place = intent.getParcelableExtra(PLACE_OBJECT);
- presenter.receive(uploadableFiles, source, place);
-
+ place = intent.getParcelableExtra(PLACE_OBJECT);
resetDirectPrefs();
}
@@ -685,39 +383,6 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
finish();
}
- /**
- * Rotates the button and shows or hides the content based on the given state. Typically used
- * for collapsing or expanding {@link CardView} animation.
- *
- * @param state the expanded state of the View whose elements are to be updated. True if
- * expanded.
- * @param button the image to rotate. Typically an arrow points up when the CardView is
- * collapsed and down when it is expanded.
- * @param content the Views that should be shown or hidden based on the state.
- */
- private void updateCardState(boolean state, ImageView button, View... content) {
- button.animate().rotation(state ? 180 : 0).start();
- if (content != null) {
- for (View view : content) {
- view.setVisibility(state ? View.VISIBLE : View.GONE);
- }
- }
- }
-
- @Override
- public List getDescriptions() {
- return descriptionsAdapter.getDescriptions();
- }
-
- private void initRecyclerView() {
- descriptionsAdapter = new DescriptionsAdapter(this);
- descriptionsAdapter.setCallback(this::showInfoAlert);
- rvDescriptions.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
- rvDescriptions.setAdapter(descriptionsAdapter);
- addNewDescription();
- }
-
-
private void showInfoAlert(int titleStringID, int messageStringId, String... formatArgs) {
new AlertDialog.Builder(this)
.setTitle(titleStringID)
@@ -729,23 +394,66 @@ public class UploadActivity extends BaseActivity implements UploadView, SimilarI
}
@Override
- public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) {
- SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
- Bundle args = new Bundle();
- args.putString("originalImagePath", originalFilePath);
- args.putString("possibleImagePath", possibleFilePath);
- newFragment.setArguments(args);
- newFragment.show(getSupportFragmentManager(), "dialog");
- }
-
- public void savePrevTitleDesc(String name){
-
- directKvStore.putString(name + "title", descriptionsAdapter.getTitle().toString());
- int n = descriptionsAdapter.getItemCount() - 1;
- directKvStore.putInt(name + "descCount", n);
- for (int i = 0; i < n; i++) {
- directKvStore.putString(name + "description_<" + i + ">", descriptionsAdapter.getDescriptions().get(i).getDescriptionText());
- directKvStore.putInt(name + "spinnerPosition_<" + i + ">", descriptionsAdapter.getDescriptions().get(i).getSelectedLanguageIndex());
+ public void onNextButtonClicked(int index) {
+ if (index < fragments.size()-1) {
+ vpUpload.setCurrentItem(index + 1, false);
+ } else {
+ presenter.handleSubmit();
}
}
+
+ @Override
+ public void onPreviousButtonClicked(int index) {
+ if (index != 0) {
+ vpUpload.setCurrentItem(index - 1, true);
+ }
+ }
+
+ /**
+ * The adapter used to show image upload intermediate fragments
+ */
+
+ private class UploadImageAdapter extends FragmentStatePagerAdapter {
+ List fragments;
+
+ public UploadImageAdapter(FragmentManager fragmentManager) {
+ super(fragmentManager);
+ this.fragments = new ArrayList<>();
+ }
+
+ public void setFragments(List fragments) {
+ this.fragments = fragments;
+ notifyDataSetChanged();
+ }
+
+ @Override public Fragment getItem(int position) {
+ return fragments.get(position);
+ }
+
+ @Override public int getCount() {
+ return fragments.size();
+ }
+
+ @Override
+ public int getItemPosition(Object object){
+ return PagerAdapter.POSITION_NONE;
+ }
+ }
+
+
+ @OnClick(R.id.rl_container_title)
+ public void onRlContainerTitleClicked(){
+ rvThumbnails.setVisibility(isTitleExpanded ? View.GONE : View.VISIBLE);
+ isTitleExpanded = !isTitleExpanded;
+ ibToggleTopCard.setRotation(ibToggleTopCard.getRotation() + 180);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ presenter.onDetachView();
+ compositeDisposable.clear();
+ mediaLicenseFragment.setCallback(null);
+ uploadCategoriesFragment.setCallback(null);
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.java
new file mode 100644
index 000000000..afd1a694b
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.java
@@ -0,0 +1,41 @@
+package fr.free.nrw.commons.upload;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import fr.free.nrw.commons.di.ApplicationlessInjection;
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
+import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
+
+/**
+ * The base fragment of the fragments in upload
+ */
+public class UploadBaseFragment extends CommonsDaggerSupportFragment {
+
+ public Callback callback;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ public void setCallback(Callback callback) {
+ this.callback = callback;
+ }
+
+ public interface Callback {
+
+ void onNextButtonClicked(int index);
+
+ void onPreviousButtonClicked(int index);
+
+ void showProgress(boolean shouldShow);
+
+ int getIndexInViewFlipper(UploadBaseFragment fragment);
+
+ int getTotalNumberOfSteps();
+
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java
new file mode 100644
index 000000000..f90496da0
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java
@@ -0,0 +1,40 @@
+package fr.free.nrw.commons.upload;
+
+import fr.free.nrw.commons.BasePresenter;
+import fr.free.nrw.commons.filepicker.UploadableFile;
+
+import java.util.List;
+
+/**
+ * The contract using which the UplaodActivity would communicate with its presenter
+ */
+public interface UploadContract {
+
+ public interface View {
+
+ boolean isLoggedIn();
+
+ void finish();
+
+ void askUserToLogIn();
+
+ void showProgress(boolean shouldShow);
+
+ void showMessage(int messageResourceId);
+
+ List getUploadableFiles();
+
+ void showHideTopCard(boolean shouldShow);
+
+ void onUploadMediaDeleted(int index);
+
+ void updateTopCardTitle();
+ }
+
+ public interface UserActionListener extends BasePresenter {
+
+ void handleSubmit();
+
+ void deletePictureAtIndex(int index);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java
index 34603f0b8..1a602a001 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java
@@ -75,7 +75,7 @@ public class UploadController {
/**
* Prepares the upload service.
*/
- void prepareService() {
+ public void prepareService() {
Intent uploadServiceIntent = new Intent(context, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
context.startService(uploadServiceIntent);
@@ -85,7 +85,7 @@ public class UploadController {
/**
* Disconnects the upload service.
*/
- void cleanup() {
+ public void cleanup() {
if (isUploadServiceConnected) {
context.unbindService(uploadServiceConnection);
}
@@ -96,7 +96,7 @@ public class UploadController {
*
* @param contribution the contribution object
*/
- void startUpload(Contribution contribution) {
+ public void startUpload(Contribution contribution) {
startUpload(contribution, c -> {});
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java
index c94f9f496..65f12354b 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java
@@ -3,16 +3,7 @@ package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
+import androidx.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
@@ -25,14 +16,20 @@ import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Observable;
import io.reactivex.Single;
-import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
-import io.reactivex.functions.Consumer;
-import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.BehaviorSubject;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
import timber.log.Timber;
-
+@Singleton
public class UploadModel {
private static UploadItem DUMMY = new UploadItem(
@@ -49,24 +46,22 @@ public class UploadModel {
private String license;
private final Map licensesByName;
private List items = new ArrayList<>();
- private boolean topCardState = true;
- private boolean bottomCardState = true;
- private boolean rightCardState = true;
private int currentStepIndex = 0;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private SessionManager sessionManager;
private FileProcessor fileProcessor;
private final ImageProcessingService imageProcessingService;
+ private List selectedCategories;
@Inject
UploadModel(@Named("licenses") List licenses,
- @Named("default_preferences") JsonKvStore store,
- @Named("licenses_by_name") Map licensesByName,
- Context context,
- SessionManager sessionManager,
- FileProcessor fileProcessor,
- ImageProcessingService imageProcessingService) {
+ @Named("default_preferences") JsonKvStore store,
+ @Named("licenses_by_name") Map licensesByName,
+ Context context,
+ SessionManager sessionManager,
+ FileProcessor fileProcessor,
+ ImageProcessingService imageProcessingService) {
this.licenses = licenses;
this.store = store;
this.license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
@@ -77,31 +72,61 @@ public class UploadModel {
this.imageProcessingService = imageProcessingService;
}
- void cleanup() {
+ /**
+ * cleanup the resources, I am Singleton, preparing for fresh upload
+ */
+ public void cleanUp() {
compositeDisposable.clear();
fileProcessor.cleanup();
+ this.items.clear();
+ if (this.selectedCategories != null) {
+ this.selectedCategories.clear();
+ }
}
+ public void setSelectedCategories(List selectedCategories) {
+ if (null == selectedCategories) {
+ selectedCategories = new ArrayList<>();
+ }
+ this.selectedCategories = selectedCategories;
+ }
+
+ /**
+ * pre process a list of items
+ */
@SuppressLint("CheckResult")
Observable preProcessImages(List uploadableFiles,
- Place place,
- String source,
- SimilarImageInterface similarImageInterface) {
- initDefaultValues();
+ Place place,
+ String source,
+ SimilarImageInterface similarImageInterface) {
return Observable.fromIterable(uploadableFiles)
- .map(uploadableFile -> getUploadItem(uploadableFile, place, source, similarImageInterface));
+ .map(uploadableFile -> getUploadItem(uploadableFile, place, source,
+ similarImageInterface));
}
- Single getImageQuality(UploadItem uploadItem, boolean checkTitle) {
+
+ /**
+ * pre process a one item at a time
+ */
+ public Observable preProcessImage(UploadableFile uploadableFile,
+ Place place,
+ String source,
+ SimilarImageInterface similarImageInterface) {
+ return Observable.just(getUploadItem(uploadableFile, place, source, similarImageInterface));
+ }
+
+ public Single getImageQuality(UploadItem uploadItem, boolean checkTitle) {
return imageProcessingService.validateImage(uploadItem, checkTitle);
}
private UploadItem getUploadItem(UploadableFile uploadableFile,
- Place place,
- String source,
- SimilarImageInterface similarImageInterface) {
- fileProcessor.initFileDetails(Objects.requireNonNull(uploadableFile.getFilePath()), context.getContentResolver());
- UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile.getFileCreatedDate(context);
+ Place place,
+ String source,
+ SimilarImageInterface similarImageInterface) {
+ fileProcessor.initFileDetails(Objects.requireNonNull(uploadableFile.getFilePath()),
+ context.getContentResolver());
+ UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile
+ .getFileCreatedDate(context);
long fileCreatedDate = -1;
String createdTimestampSource = "";
if (dateTimeWithSource != null) {
@@ -109,52 +134,21 @@ public class UploadModel {
createdTimestampSource = dateTimeWithSource.getSource();
}
Timber.d("File created date is %d", fileCreatedDate);
- GPSExtractor gpsExtractor = fileProcessor.processFileCoordinates(similarImageInterface, context);
- return new UploadItem(uploadableFile.getContentUri(), Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate, createdTimestampSource);
- }
-
- void onItemsProcessed(Place place, List uploadItems) {
- items = uploadItems;
- if (items.isEmpty()) {
- return;
- }
-
- UploadItem uploadItem = items.get(0);
- uploadItem.selected = true;
- uploadItem.first = true;
-
+ GPSExtractor gpsExtractor = fileProcessor
+ .processFileCoordinates(similarImageInterface, context);
+ UploadItem uploadItem = new UploadItem(uploadableFile.getContentUri(),
+ Uri.parse(uploadableFile.getFilePath()),
+ uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate,
+ createdTimestampSource);
if (place != null) {
- uploadItem.title.setTitleText(place.getName());
- uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription().equals("?")?"":place.getLongDescription());
- //TODO figure out if default descriptions in other languages exist
+ uploadItem.title.setTitleText(place.name);
+ uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription());
uploadItem.descriptions.get(0).setLanguageCode("en");
}
- }
-
- private void initDefaultValues() {
- currentStepIndex = 0;
- topCardState = true;
- bottomCardState = true;
- rightCardState = true;
- items = new ArrayList<>();
- }
-
- boolean isPreviousAvailable() {
- return currentStepIndex > 0;
- }
-
- boolean isNextAvailable() {
- return currentStepIndex < (items.size() + 1);
- }
-
- boolean isSubmitAvailable() {
- int count = items.size();
- boolean hasError = license == null;
- for (int i = 0; i < count; i++) {
- UploadItem item = items.get(i);
- hasError |= item.error;
+ if (!items.contains(uploadItem)) {
+ items.add(uploadItem);
}
- return !hasError;
+ return uploadItem;
}
int getCurrentStep() {
@@ -173,110 +167,20 @@ public class UploadModel {
return items;
}
- boolean isTopCardState() {
- return topCardState;
- }
-
- void setTopCardState(boolean topCardState) {
- this.topCardState = topCardState;
- }
-
- boolean isBottomCardState() {
- return bottomCardState;
- }
-
- void setRightCardState(boolean rightCardState) {
- this.rightCardState = rightCardState;
- }
-
- boolean isRightCardState() {
- return rightCardState;
- }
-
- void setBottomCardState(boolean bottomCardState) {
- this.bottomCardState = bottomCardState;
- }
-
- @SuppressLint("CheckResult")
- public void next() {
- markCurrentUploadVisited();
- if (currentStepIndex < items.size() + 1) {
- currentStepIndex++;
- }
- updateItemState();
- }
-
- void setCurrentTitleAndDescriptions(Title title, List descriptions) {
- setCurrentUploadTitle(title);
- setCurrentUploadDescriptions(descriptions);
- }
-
- private void setCurrentUploadTitle(Title title) {
- if (currentStepIndex < items.size() && currentStepIndex >= 0) {
- items.get(currentStepIndex).title = title;
- }
- }
-
- private void setCurrentUploadDescriptions(List descriptions) {
- if (currentStepIndex < items.size() && currentStepIndex >= 0) {
- items.get(currentStepIndex).descriptions = descriptions;
- }
- }
-
- public void previous() {
- cleanup();
- markCurrentUploadVisited();
- if (currentStepIndex > 0) {
- currentStepIndex--;
- }
- updateItemState();
- }
-
- void jumpTo(UploadItem item) {
- currentStepIndex = items.indexOf(item);
- item.visited = true;
- updateItemState();
- }
-
- UploadItem getCurrentItem() {
- return isShowingItem() ? items.get(currentStepIndex) : DUMMY;
- }
-
- boolean isShowingItem() {
- return currentStepIndex < items.size();
- }
-
- private void updateItemState() {
- Timber.d("Updating item state");
- int count = items.size();
- for (int i = 0; i < count; i++) {
- UploadItem item = items.get(i);
- item.selected = (currentStepIndex >= count || i == currentStepIndex);
- item.error = item.title == null || item.title.isEmpty();
- }
- }
-
- private void markCurrentUploadVisited() {
- Timber.d("Marking current upload visited");
- if (currentStepIndex < items.size() && currentStepIndex >= 0) {
- items.get(currentStepIndex).visited = true;
- }
- }
-
public List getLicenses() {
return licenses;
}
- String getSelectedLicense() {
+ public String getSelectedLicense() {
return license;
}
- void setSelectedLicense(String licenseName) {
+ public void setSelectedLicense(String licenseName) {
this.license = licensesByName.get(licenseName);
store.putString(Prefs.DEFAULT_LICENSE, license);
}
- Observable buildContributions(List categoryStringList) {
+ public Observable buildContributions() {
return Observable.fromIterable(items).map(item ->
{
Contribution contribution = new Contribution(item.mediaUri, null,
@@ -287,7 +191,10 @@ public class UploadModel {
if (item.place != null) {
contribution.setWikiDataEntityId(item.place.getWikiDataEntityId());
}
- contribution.setCategories(categoryStringList);
+ if (null == selectedCategories) {//Just a fail safe, this should never be null
+ selectedCategories = new ArrayList<>();
+ }
+ contribution.setCategories(selectedCategories);
contribution.setTag("mimeType", item.mimeType);
contribution.setSource(item.source);
contribution.setContentProviderUri(item.mediaUri);
@@ -304,21 +211,16 @@ public class UploadModel {
});
}
- void keepPicture() {
- items.get(currentStepIndex).setImageQuality(ImageUtils.IMAGE_KEEP);
- }
-
- void deletePicture() {
- cleanup();
- updateItemState();
- }
-
- void subscribeBadPicture(Consumer consumer, boolean checkTitle) {
- if (isShowingItem()) {
- compositeDisposable.add(getImageQuality(getCurrentItem(), checkTitle)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(consumer, Timber::e));
+ public void deletePicture(String filePath) {
+ Iterator iterator = items.iterator();
+ while (iterator.hasNext()) {
+ if (iterator.next().mediaUri.toString().contains(filePath)) {
+ iterator.remove();
+ break;
+ }
+ }
+ if (items.isEmpty()) {
+ cleanUp();
}
}
@@ -326,8 +228,15 @@ public class UploadModel {
return items;
}
+ public void updateUploadItem(int index, UploadItem uploadItem) {
+ UploadItem uploadItem1 = items.get(index);
+ uploadItem1.setDescriptions(uploadItem.descriptions);
+ uploadItem1.setTitle(uploadItem.title);
+ }
+
@SuppressWarnings("WeakerAccess")
- static class UploadItem {
+ public static class UploadItem {
+
private final Uri originalContentUri;
private final Uri mediaUri;
private final String mimeType;
@@ -347,10 +256,10 @@ public class UploadModel {
@SuppressLint("CheckResult")
UploadItem(Uri originalContentUri,
- Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords,
- Place place,
- long createdTimestamp,
- String createdTimestampSource) {
+ Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords,
+ Place place,
+ long createdTimestamp,
+ String createdTimestampSource) {
this.originalContentUri = originalContentUri;
this.createdTimestampSource = createdTimestampSource;
title = new Title();
@@ -426,16 +335,40 @@ public class UploadModel {
}
public String getFileName() {
- return Utils.fixExtension(title.toString(), getFileExt());
+ return title
+ != null ? Utils.fixExtension(title.toString(), getFileExt()) : null;
}
public Place getPlace() {
return place;
}
+ public void setTitle(Title title) {
+ this.title = title;
+ }
+
+ public void setDescriptions(List descriptions) {
+ this.descriptions = descriptions;
+ }
+
public Uri getContentUri() {
return originalContentUri;
}
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof UploadItem)) {
+ return false;
+ }
+ return this.mediaUri.toString().contains(((UploadItem) (obj)).mediaUri.toString());
+
+ }
+
+ //Travis is complaining :P
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.java
new file mode 100644
index 000000000..9e4f572a7
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.java
@@ -0,0 +1,36 @@
+package fr.free.nrw.commons.upload;
+
+import dagger.Binds;
+import dagger.Module;
+import fr.free.nrw.commons.upload.categories.CategoriesContract;
+import fr.free.nrw.commons.upload.categories.CategoriesPresenter;
+import fr.free.nrw.commons.upload.license.MediaLicenseContract;
+import fr.free.nrw.commons.upload.license.MediaLicensePresenter;
+import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract;
+import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter;
+
+/**
+ * The Dagger Module for upload related presenters and (some other objects maybe in future)
+ */
+@Module
+public abstract class UploadModule {
+
+ @Binds
+ public abstract UploadContract.UserActionListener bindHomePresenter(UploadPresenter
+ presenter);
+
+ @Binds
+ public abstract CategoriesContract.UserActionListener bindsCategoriesPresenter(CategoriesPresenter
+ presenter);
+
+ @Binds
+ public abstract MediaLicenseContract.UserActionListener bindsMediaLicensePresenter(
+ MediaLicensePresenter
+ presenter);
+
+ @Binds
+ public abstract UploadMediaDetailsContract.UserActionListener bindsUploadMediaPresenter(
+ UploadMediaPresenter
+ presenter);
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java
index 5e0dd3231..a08a547a9 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java
@@ -1,420 +1,126 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
-import android.content.Context;
-import android.text.TextUtils;
+
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.Utils;
-import fr.free.nrw.commons.category.CategoriesModel;
import fr.free.nrw.commons.contributions.Contribution;
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.utils.CustomProxy;
-import fr.free.nrw.commons.utils.CustomProxy;
-import fr.free.nrw.commons.utils.StringSortingUtils;
-import io.reactivex.Observable;
-import io.reactivex.android.schedulers.AndroidSchedulers;
+import fr.free.nrw.commons.repository.UploadRepository;
+import io.reactivex.Observer;
import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
-import java.util.ArrayList;
-import java.util.List;
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-import org.apache.commons.lang3.StringUtils;
-import java.util.ArrayList;
-import java.util.List;
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
import timber.log.Timber;
import static fr.free.nrw.commons.upload.UploadModel.UploadItem;
-import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
-import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
-import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
-import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
-import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
/**
* The MVP pattern presenter of Upload GUI
*/
@Singleton
-public class UploadPresenter {
+public class UploadPresenter implements UploadContract.UserActionListener {
- private static final UploadView DUMMY =
- (UploadView) CustomProxy.newInstance(UploadView.class.getClassLoader(),
- new Class[] { UploadView.class });
+ private static final UploadContract.View DUMMY = (UploadContract.View) Proxy.newProxyInstance(
+ UploadContract.View.class.getClassLoader(),
+ new Class[]{UploadContract.View.class}, (proxy, method, methodArgs) -> null);
+ private final UploadRepository repository;
+ private UploadContract.View view = DUMMY;
- private UploadView view = DUMMY;
-
- private static final SimilarImageInterface SIMILAR_IMAGE =
- (SimilarImageInterface) CustomProxy.newInstance(
- SimilarImageInterface.class.getClassLoader(),
- new Class[] { SimilarImageInterface.class });
- private SimilarImageInterface similarImageInterface = SIMILAR_IMAGE;
-
- @UploadView.UploadPage
- private int currentPage = UploadView.PLEASE_WAIT;
-
- private final UploadModel uploadModel;
- private final UploadController uploadController;
- private final Context context;
- private final JsonKvStore directKvStore;
- private CompositeDisposable compositeDisposable = new CompositeDisposable();
+ private CompositeDisposable compositeDisposable;
@Inject
- UploadPresenter(UploadModel uploadModel,
- UploadController uploadController,
- Context context,
- @Named("default_preferences") JsonKvStore directKvStore) {
- this.uploadModel = uploadModel;
- this.uploadController = uploadController;
- this.context = context;
- this.directKvStore = directKvStore;
+ UploadPresenter(UploadRepository uploadRepository) {
+ this.repository = uploadRepository;
+ compositeDisposable = new CompositeDisposable();
}
- /**
- * Passes the items received to {@link #uploadModel} and displays the items.
- *
- * @param media The Uri's of the media being uploaded.
- * @param source File source from {@link Contribution.FileSource}
- */
- @SuppressLint("CheckResult")
- void receive(List media,
- @Contribution.FileSource String source,
- Place place) {
- Observable uploadItemObservable = uploadModel
- .preProcessImages(media, place, source, similarImageInterface);
-
- compositeDisposable.add(uploadItemObservable
- .toList()
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(uploadItems -> onImagesProcessed(uploadItems, place),
- throwable -> Timber.e(throwable, "Error occurred in processing images")));
- }
-
- private void onImagesProcessed(List uploadItems, Place place) {
- uploadModel.onItemsProcessed(place, uploadItems);
- updateCards();
- updateLicenses();
- updateContent();
- uploadModel.subscribeBadPicture(this::handleBadImage, false);
- }
-
- /**
- * Sets the license to parameter and updates {@link UploadActivity}
- *
- * @param licenseName license name
- */
- void selectLicense(String licenseName) {
- uploadModel.setSelectedLicense(licenseName);
- view.updateLicenseSummary(uploadModel.getSelectedLicense(), uploadModel.getCount());
- }
-
- //region Wizard step management
-
- /**
- * Called by the next button in {@link UploadActivity}
- */
- @SuppressLint("CheckResult")
- void handleNext(Title title,
- List descriptions) {
- Timber.e("Inside handleNext");
- view.showProgressDialog();
- setTitleAndDescription(title, descriptions);
- compositeDisposable.add(uploadModel.getImageQuality(uploadModel.getCurrentItem(), true)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(imageResult -> handleImage(title, descriptions, imageResult),
- throwable -> Timber.e(throwable, "Error occurred while handling image")));
- }
-
- private void handleImage(Title title, List descriptions, Integer imageResult) {
- view.hideProgressDialog();
- if (imageResult == IMAGE_KEEP || imageResult == IMAGE_OK) {
- Timber.d("Set title and desc; Show next uploaded item");
- setTitleAndDescription(title, descriptions);
- directKvStore.putBoolean("Picture_Has_Correct_Location", true);
- nextUploadedItem();
- } else {
- handleBadImage(imageResult);
- }
- }
-
- /**
- * Called by the next button in {@link UploadActivity}
- */
- @SuppressLint("CheckResult")
- void handleCategoryNext(CategoriesModel categoriesModel,
- boolean noCategoryWarningShown) {
- if (categoriesModel.selectedCategoriesCount() < 1 && !noCategoryWarningShown) {
- view.showNoCategorySelectedWarning();
- } else {
- nextUploadedItem();
- }
- }
-
- private void handleBadImage(Integer errorCode) {
- Timber.d("Handle bad picture with error code %d", errorCode);
- if (errorCode >= 8) { // If location of image and nearby does not match, then set shared preferences to disable wikidata edits
- directKvStore.putBoolean("Picture_Has_Correct_Location", false);
- }
-
- switch (errorCode) {
- case EMPTY_TITLE:
- Timber.d("Title is empty. Showing toast");
- view.showErrorMessage(R.string.add_title_toast);
- break;
- case FILE_NAME_EXISTS:
- Timber.d("Trying to show duplicate picture popup");
- view.showDuplicatePicturePopup();
- break;
- default:
- String errorMessageForResult = getErrorMessageForResult(context, errorCode);
- if (TextUtils.isEmpty(errorMessageForResult)) {
- return;
- }
- view.showBadPicturePopup(errorMessageForResult);
- }
- }
-
- private void nextUploadedItem() {
- Timber.d("Trying to show next uploaded item");
- uploadModel.next();
- updateContent();
- uploadModel.subscribeBadPicture(this::handleBadImage, false);
- view.dismissKeyboard();
- }
-
- private void setTitleAndDescription(Title title, List descriptions) {
- Timber.d("setTitleAndDescription: Setting title and desc");
- uploadModel.setCurrentTitleAndDescriptions(title, descriptions);
- }
-
- String getCurrentImageFileName() {
- UploadItem currentItem = getCurrentItem();
- return currentItem.getFileName();
- }
-
- /**
- * Called by the previous button in {@link UploadActivity}
- */
- void handlePrevious() {
- uploadModel.previous();
- updateContent();
- uploadModel.subscribeBadPicture(this::handleBadImage, false);
- view.dismissKeyboard();
- }
-
- /**
- * Called when one of the pictures on the top card is clicked on in {@link UploadActivity}
- */
- void thumbnailClicked(UploadItem item) {
- uploadModel.jumpTo(item);
- updateContent();
- }
/**
* Called by the submit button in {@link UploadActivity}
*/
@SuppressLint("CheckResult")
- void handleSubmit(CategoriesModel categoriesModel) {
- if (view.checkIfLoggedIn())
- compositeDisposable.add(uploadModel.buildContributions(categoriesModel.getCategoryStringList())
+ @Override
+ public void handleSubmit() {
+ if (view.isLoggedIn()) {
+ view.showProgress(true);
+ repository.buildContributions()
.observeOn(Schedulers.io())
- .subscribe(uploadController::startUpload));
- }
+ .subscribe(new Observer() {
+ @Override
+ public void onSubscribe(Disposable d) {
+ view.showProgress(false);
+ view.showMessage(R.string.uploading_started);
+ compositeDisposable.add(d);
+ }
- /**
- * Called by the map button on the right card in {@link UploadActivity}
- */
- void openCoordinateMap() {
- GPSExtractor gpsObj = uploadModel.getCurrentItem().getGpsCoords();
- if (gpsObj != null && gpsObj.imageCoordsExists) {
- view.launchMapActivity(new LatLng(gpsObj.getDecLatitude(), gpsObj.getDecLongitude(), 0.0f));
- }
- }
+ @Override
+ public void onNext(Contribution contribution) {
+ repository.startUpload(contribution);
+ }
- void keepPicture() {
- uploadModel.keepPicture();
- }
+ @Override
+ public void onError(Throwable e) {
+ view.showMessage(R.string.upload_failed);
+ repository.cleanup();
+ view.finish();
+ compositeDisposable.clear();
+ Timber.e("failed to upload: " + e.getMessage());
+ }
- void deletePicture() {
- if (uploadModel.getCount() == 1)
- view.finish();
- else {
- uploadModel.deletePicture();
- updateCards();
- updateContent();
- uploadModel.subscribeBadPicture(this::handleBadImage, false);
- view.dismissKeyboard();
- }
- }
- //endregion
-
- //region Top Bottom and Right card state management
-
-
- /**
- * Toggles the top card's state between open and closed.
- */
- void toggleTopCardState() {
- uploadModel.setTopCardState(!uploadModel.isTopCardState());
- view.setTopCardState(uploadModel.isTopCardState());
- }
-
- /**
- * Toggles the bottom card's state between open and closed.
- */
- void toggleBottomCardState() {
- uploadModel.setBottomCardState(!uploadModel.isBottomCardState());
- view.setBottomCardState(uploadModel.isBottomCardState());
- }
-
- /**
- * Sets all the cards' states to closed.
- */
- void closeAllCards() {
- if (uploadModel.isTopCardState()) {
- uploadModel.setTopCardState(false);
- view.setTopCardState(false);
- }
- if (uploadModel.isRightCardState()) {
- uploadModel.setRightCardState(false);
- }
- if (uploadModel.isBottomCardState()) {
- uploadModel.setBottomCardState(false);
- view.setBottomCardState(false);
- }
- }
- //endregion
-
- //region View / Lifecycle management
- public void init() {
- uploadController.prepareService();
- }
-
- void cleanup() {
- compositeDisposable.clear();
- uploadModel.cleanup();
- uploadController.cleanup();
- }
-
- void removeView() {
- this.view = DUMMY;
- }
-
- void addView(UploadView view) {
- this.view = view;
-
- updateCards();
- updateLicenses();
- updateContent();
- }
-
-
- /**
- * Updates the cards for when there is a change to the amount of items being uploaded.
- */
- private void updateCards() {
- Timber.i("uploadModel.getCount():" + uploadModel.getCount());
- view.updateThumbnails(uploadModel.getUploads());
- view.setTopCardVisibility(uploadModel.getCount() > 1);
- view.setBottomCardVisibility(uploadModel.getCount() > 0);
- view.setTopCardState(uploadModel.isTopCardState());
- view.setBottomCardState(uploadModel.isBottomCardState());
- }
-
- /**
- * Sets the list of licences and the default license.
- */
- private void updateLicenses() {
- String selectedLicense = directKvStore.getString(Prefs.DEFAULT_LICENSE,
- Prefs.Licenses.CC_BY_SA_4);//CC_BY_SA_4 is the default one used by the commons web app
- try {//I have to make sure that the stored default license was not one of the deprecated one's
- Utils.licenseNameFor(selectedLicense);
- } catch (IllegalStateException exception) {
- Timber.e(exception.getMessage());
- selectedLicense = Prefs.Licenses.CC_BY_SA_4;
- directKvStore.putString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4);
- }
- view.updateLicenses(uploadModel.getLicenses(), selectedLicense);
- view.updateLicenseSummary(selectedLicense, uploadModel.getCount());
- }
-
- /**
- * Updates the cards and the background when a new currentPage is selected.
- */
- private void updateContent() {
- Timber.i("Updating content for currentPage" + uploadModel.getCurrentStep());
- view.setNextEnabled(uploadModel.isNextAvailable());
- view.setPreviousEnabled(uploadModel.isPreviousAvailable());
- view.setSubmitEnabled(uploadModel.isSubmitAvailable());
-
- view.setBackground(uploadModel.getCurrentItem().getMediaUri());
-
- view.updateBottomCardContent(uploadModel.getCurrentStep(),
- uploadModel.getStepCount(),
- uploadModel.getCurrentItem(),
- uploadModel.isShowingItem());
-
- view.updateTopCardContent();
-
- GPSExtractor gpsObj = uploadModel.getCurrentItem().getGpsCoords();
- view.updateRightCardContent(gpsObj != null && gpsObj.imageCoordsExists);
-
- view.updateSubtitleVisibility(uploadModel.getCount());
-
- showCorrectCards(uploadModel.getCurrentStep(), uploadModel.getCount());
- }
-
- /**
- * Updates the layout to show the correct bottom card.
- *
- * @param currentStep the current step
- * @param uploadCount how many items are being uploaded
- */
- private void showCorrectCards(int currentStep, int uploadCount) {
- if (uploadCount == 0) {
- currentPage = UploadView.PLEASE_WAIT;
- } else if (currentStep <= uploadCount) {
- currentPage = UploadView.TITLE_CARD;
- view.setTopCardVisibility(uploadModel.getCount() > 1);
- } else if (currentStep == uploadCount + 1) {
- currentPage = UploadView.CATEGORIES;
- view.setTopCardVisibility(false);
- view.setRightCardVisibility(false);
- view.initDefaultCategories();
+ @Override
+ public void onComplete() {
+ repository.cleanup();
+ view.finish();
+ compositeDisposable.clear();
+ }
+ });
} else {
- currentPage = UploadView.LICENSE;
- view.setTopCardVisibility(false);
- view.setRightCardVisibility(false);
+ view.askUserToLogIn();
}
- view.setBottomCardVisibility(currentPage, uploadCount);
}
- //endregion
+ @Override
+ public void deletePictureAtIndex(int index) {
+ List uploadableFiles = view.getUploadableFiles();
+ if (index == uploadableFiles.size() - 1) {//If the next fragment to be shown is not one of the MediaDetailsFragment, lets hide the top card
+ view.showHideTopCard(false);
+ }
+ //Ask the repository to delete the picture
+ repository.deletePicture(uploadableFiles.get(index).getFilePath());
+ if (uploadableFiles.size() == 1) {
+ view.showMessage(R.string.upload_cancelled);
+ view.finish();
+ return;
+ } else {
+ view.onUploadMediaDeleted(index);
+ }
+ if (uploadableFiles.size() < 2) {
+ view.showHideTopCard(false);
+ }
+
+ //In case lets update the number of uploadable media
+ view.updateTopCardTitle();
- /**
- * @return the item currently being displayed
- */
- private UploadItem getCurrentItem() {
- return uploadModel.getCurrentItem();
}
- List getImageTitleList() {
- List titleList = new ArrayList<>();
- for (UploadItem item : uploadModel.getUploads()) {
- if (item.getTitle().isSet()) {
- titleList.add(item.getTitle().toString());
- }
- }
- return titleList;
+ @Override
+ public void onAttachView(UploadContract.View view) {
+ this.view = view;
+ repository.prepareService();
+ }
+
+ @Override
+ public void onDetachView() {
+ this.view = DUMMY;
+ compositeDisposable.clear();
+ repository.cleanup();
}
}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java
deleted file mode 100644
index afcc42aed..000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.net.Uri;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-
-import com.facebook.drawee.view.SimpleDraweeView;
-import com.pedrogomez.renderers.Renderer;
-
-import java.io.File;
-
-import fr.free.nrw.commons.R;
-
-class UploadThumbnailRenderer extends Renderer {
- private ThumbnailClickedListener listener;
- private SimpleDraweeView background;
- private View space;
- private ImageView error;
-
- public UploadThumbnailRenderer(ThumbnailClickedListener listener) {
- this.listener = listener;
- }
-
- @Override
- protected View inflate(LayoutInflater inflater, ViewGroup parent) {
- return inflater.inflate(R.layout.item_upload_thumbnail, parent, false);
- }
-
- @Override
- protected void setUpView(View rootView) {
- error = rootView.findViewById(R.id.error);
- space = rootView.findViewById(R.id.left_space);
- background = rootView.findViewById(R.id.thumbnail);
- }
-
- @Override
- protected void hookListeners(View rootView) {
- background.setOnClickListener(v -> listener.thumbnailClicked(getContent()));
- }
-
- @Override
- public void render() {
- UploadModel.UploadItem content = getContent();
- Uri uri = Uri.parse(content.getMediaUri().toString());
- background.setImageURI(Uri.fromFile(new File(String.valueOf(uri))));
- background.setAlpha(content.isSelected() ? 1.0f : 0.5f);
- space.setVisibility(content.isFirst() ? View.VISIBLE : View.GONE);
- error.setVisibility(content.isVisited() && content.isError() ? View.VISIBLE : View.GONE);
- }
-
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java
index 3ea4dfa62..e69de29bb 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java
@@ -1,24 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import com.pedrogomez.renderers.ListAdapteeCollection;
-import com.pedrogomez.renderers.RVRendererAdapter;
-import com.pedrogomez.renderers.RendererBuilder;
-
-import java.util.Collections;
-import java.util.List;
-
-public class UploadThumbnailsAdapterFactory {
- private ThumbnailClickedListener listener;
-
- UploadThumbnailsAdapterFactory(ThumbnailClickedListener listener) {
- this.listener = listener;
- }
-
- public RVRendererAdapter create(List placeList) {
- RendererBuilder builder = new RendererBuilder()
- .bind(UploadModel.UploadItem.class, new UploadThumbnailRenderer(listener));
- ListAdapteeCollection collection = new ListAdapteeCollection<>(
- placeList != null ? placeList : Collections.emptyList());
- return new RVRendererAdapter<>(builder, collection);
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java
index 9fb50c7ca..ec1854ffc 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java
@@ -15,7 +15,6 @@ public interface UploadView {
// UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
// new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null);
- List getDescriptions();
@Retention(SOURCE)
@IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE})
@@ -82,4 +81,6 @@ public interface UploadView {
void showProgressDialog();
void hideProgressDialog();
+
+ void askUserToLogIn();
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.java
new file mode 100644
index 000000000..6ff51632a
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.java
@@ -0,0 +1,42 @@
+package fr.free.nrw.commons.upload.categories;
+
+import fr.free.nrw.commons.BasePresenter;
+import fr.free.nrw.commons.category.CategoryItem;
+
+import java.util.List;
+
+/**
+ * The contract with with UploadCategoriesFragment and its presenter would talk to each other
+ */
+public interface CategoriesContract {
+
+ public interface View {
+
+ void showProgress(boolean shouldShow);
+
+ void showError(String error);
+
+ void showError(int stringResourceId);
+
+ void setCategories(List categories);
+
+ void addCategory(CategoryItem category);
+
+ void goToNextScreen();
+
+ void showNoCategorySelected();
+
+ void setSelectedCategories(List selectedCategories);
+ }
+
+ public interface UserActionListener extends BasePresenter {
+
+ void searchForCategories(String query);
+
+ void verifyCategories();
+
+ void onCategoryItemClicked(CategoryItem categoryItem);
+ }
+
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.java
new file mode 100644
index 000000000..a0a776246
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.java
@@ -0,0 +1,144 @@
+package fr.free.nrw.commons.upload.categories;
+
+import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
+import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
+
+import android.text.TextUtils;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.category.CategoryItem;
+import fr.free.nrw.commons.repository.UploadRepository;
+import fr.free.nrw.commons.upload.UploadModel.UploadItem;
+import io.reactivex.Observable;
+import io.reactivex.Scheduler;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.List;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import timber.log.Timber;
+
+/**
+ * The presenter class for UploadCategoriesFragment
+ */
+@Singleton
+public class CategoriesPresenter implements CategoriesContract.UserActionListener {
+
+ private static final CategoriesContract.View DUMMY = (CategoriesContract.View) Proxy
+ .newProxyInstance(
+ CategoriesContract.View.class.getClassLoader(),
+ new Class[]{CategoriesContract.View.class},
+ (proxy, method, methodArgs) -> null);
+ private final Scheduler ioScheduler;
+ private final Scheduler mainThreadScheduler;
+
+ CategoriesContract.View view = DUMMY;
+ private UploadRepository repository;
+
+ private CompositeDisposable compositeDisposable;
+
+ @Inject
+ public CategoriesPresenter(UploadRepository repository, @Named(IO_THREAD) Scheduler ioScheduler,
+ @Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
+ this.repository = repository;
+ this.ioScheduler = ioScheduler;
+ this.mainThreadScheduler = mainThreadScheduler;
+ compositeDisposable = new CompositeDisposable();
+ }
+
+ @Override
+ public void onAttachView(CategoriesContract.View view) {
+ this.view = view;
+ }
+
+ @Override
+ public void onDetachView() {
+ this.view = DUMMY;
+ compositeDisposable.clear();
+ }
+
+ /**
+ * asks the repository to fetch categories for the query
+ * @param query
+ *
+ */
+ @Override
+ public void searchForCategories(String query) {
+ List categoryItems = new ArrayList<>();
+ List imageTitleList = getImageTitleList();
+ Observable distinctCategoriesObservable = Observable
+ .fromIterable(repository.getSelectedCategories())
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .doOnSubscribe(disposable -> {
+ view.showProgress(true);
+ view.showError(null);
+ view.setCategories(null);
+ })
+ .observeOn(ioScheduler)
+ .concatWith(
+ repository.searchAll(query, imageTitleList)
+ )
+ .filter(categoryItem -> !repository.containsYear(categoryItem.getName()))
+ .distinct();
+ if(!TextUtils.isEmpty(query)) {
+ distinctCategoriesObservable=distinctCategoriesObservable.sorted(repository.sortBySimilarity(query));
+ }
+ Disposable searchCategoriesDisposable = distinctCategoriesObservable
+ .observeOn(mainThreadScheduler)
+ .subscribe(
+ s -> categoryItems.add(s),
+ Timber::e,
+ () -> {
+ view.setCategories(categoryItems);
+ view.showProgress(false);
+
+ if (categoryItems.isEmpty()) {
+ view.showError(R.string.no_categories_found);
+ }
+ }
+ );
+
+ compositeDisposable.add(searchCategoriesDisposable);
+ }
+
+ /**
+ * Returns image title list from UploadItem
+ * @return
+ */
+ private List getImageTitleList() {
+ List titleList = new ArrayList<>();
+ for (UploadItem item : repository.getUploads()) {
+ if (item.getTitle().isSet()) {
+ titleList.add(item.getTitle().toString());
+ }
+ }
+ return titleList;
+ }
+
+ /**
+ * Verifies the number of categories selected, prompts the user if none selected
+ */
+ @Override
+ public void verifyCategories() {
+ List selectedCategories = repository.getSelectedCategories();
+ if (selectedCategories != null && !selectedCategories.isEmpty()) {
+ repository.setSelectedCategories(repository.getCategoryStringList());
+ view.goToNextScreen();
+ } else {
+ view.showNoCategorySelected();
+ }
+ }
+
+ /**
+ * ask repository to handle category clicked
+ *
+ * @param categoryItem
+ */
+ @Override
+ public void onCategoryItemClicked(CategoryItem categoryItem) {
+ repository.onCategoryClicked(categoryItem);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java
new file mode 100644
index 000000000..48485c17a
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java
@@ -0,0 +1,200 @@
+package fr.free.nrw.commons.upload.categories;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+import com.google.android.material.textfield.TextInputEditText;
+import com.google.android.material.textfield.TextInputLayout;
+import com.jakewharton.rxbinding2.view.RxView;
+import com.jakewharton.rxbinding2.widget.RxTextView;
+import com.pedrogomez.renderers.RVRendererAdapter;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.category.CategoryClickedListener;
+import fr.free.nrw.commons.category.CategoryItem;
+import fr.free.nrw.commons.upload.UploadBaseFragment;
+import fr.free.nrw.commons.upload.UploadCategoriesAdapterFactory;
+import fr.free.nrw.commons.utils.DialogUtil;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import timber.log.Timber;
+
+public class UploadCategoriesFragment extends UploadBaseFragment implements CategoriesContract.View,
+ CategoryClickedListener {
+
+ @BindView(R.id.tv_title)
+ TextView tvTitle;
+ @BindView(R.id.til_container_search)
+ TextInputLayout tilContainerEtSearch;
+ @BindView(R.id.et_search)
+ TextInputEditText etSearch;
+ @BindView(R.id.pb_categories)
+ ProgressBar pbCategories;
+ @BindView(R.id.rv_categories)
+ RecyclerView rvCategories;
+
+ @Inject
+ CategoriesContract.UserActionListener presenter;
+ private RVRendererAdapter adapter;
+ private List mediaTitleList=new ArrayList<>();
+ private Disposable subscribe;
+ private List categories;
+ private boolean isVisible;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ public void setMediaTitleList(List mediaTitleList) {
+ this.mediaTitleList = mediaTitleList;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.upload_categories_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ButterKnife.bind(this, view);
+ init();
+ }
+
+ private void init() {
+ tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
+ callback.getTotalNumberOfSteps()));
+ presenter.onAttachView(this);
+ initRecyclerView();
+ addTextChangeListenerToEtSearch();
+ //get default categories for empty query
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (presenter != null && isVisible && (categories == null || categories.isEmpty())) {
+ presenter.searchForCategories(null);
+ }
+ }
+
+ private void addTextChangeListenerToEtSearch() {
+ subscribe = RxTextView.textChanges(etSearch)
+ .doOnEach(v -> tilContainerEtSearch.setError(null))
+ .takeUntil(RxView.detaches(etSearch))
+ .debounce(500, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(filter -> searchForCategory(filter.toString()), Timber::e);
+ }
+
+ private void searchForCategory(String query) {
+ presenter.searchForCategories(query);
+ }
+
+ private void initRecyclerView() {
+ adapter = new UploadCategoriesAdapterFactory(this)
+ .create(new ArrayList<>());
+ rvCategories.setLayoutManager(new LinearLayoutManager(getContext()));
+ rvCategories.setAdapter(adapter);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ presenter.onDetachView();
+ subscribe.dispose();
+ }
+
+ @Override
+ public void showProgress(boolean shouldShow) {
+ pbCategories.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void showError(String error) {
+ tilContainerEtSearch.setError(error);
+ }
+
+ @Override
+ public void showError(int stringResourceId) {
+ tilContainerEtSearch.setError(getString(stringResourceId));
+ }
+
+ @Override
+ public void setCategories(List categories) {
+ adapter.clear();
+ if (categories != null) {
+ this.categories = categories;
+ adapter.addAll(categories);
+ adapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void addCategory(CategoryItem category) {
+ adapter.add(category);
+ adapter.notifyItemInserted(adapter.getItemCount());
+ }
+
+ @Override
+ public void goToNextScreen() {
+ callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
+ }
+
+ @Override
+ public void showNoCategorySelected() {
+ DialogUtil.showAlertDialog(getActivity(),
+ getString(R.string.no_categories_selected),
+ getString(R.string.no_categories_selected_warning_desc),
+ getString(R.string.no_go_back),
+ getString(R.string.yes_submit),
+ null,
+ () -> goToNextScreen());
+ }
+
+ @Override
+ public void setSelectedCategories(List selectedCategories) {
+
+ }
+
+ @OnClick(R.id.btn_next)
+ public void onNextButtonClicked() {
+ presenter.verifyCategories();
+ }
+
+ @OnClick(R.id.btn_previous)
+ public void onPreviousButtonClicked() {
+ callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
+ }
+
+ @Override
+ public void categoryClicked(CategoryItem item) {
+ presenter.onCategoryItemClicked(item);
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ super.setUserVisibleHint(isVisibleToUser);
+ isVisible = isVisibleToUser;
+
+ if (presenter != null && isResumed() && (categories == null || categories.isEmpty())) {
+ presenter.searchForCategories(null);
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.java
new file mode 100644
index 000000000..68e6affb4
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.java
@@ -0,0 +1,26 @@
+package fr.free.nrw.commons.upload.license;
+
+import fr.free.nrw.commons.BasePresenter;
+
+import java.util.List;
+
+/**
+ * The contract with with MediaLicenseFragment and its presenter would talk to each other
+ */
+public interface MediaLicenseContract {
+
+ interface View {
+ void setLicenses(List licenses);
+
+ void setSelectedLicense(String license);
+
+ void updateLicenseSummary(String selectedLicense, int numberOfItems);
+ }
+
+ interface UserActionListener extends BasePresenter {
+ void getLicenses();
+
+ void selectLicense(String licenseName);
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java
new file mode 100644
index 000000000..8836c9bdf
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java
@@ -0,0 +1,181 @@
+package fr.free.nrw.commons.upload.license;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.SpannableStringBuilder;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
+import android.text.style.URLSpan;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.Utils;
+import fr.free.nrw.commons.upload.UploadBaseFragment;
+import timber.log.Timber;
+
+public class MediaLicenseFragment extends UploadBaseFragment implements MediaLicenseContract.View {
+
+ @BindView(R.id.tv_title)
+ TextView tvTitle;
+ @BindView(R.id.spinner_license_list)
+ Spinner spinnerLicenseList;
+ @BindView(R.id.tv_share_license_summary)
+ TextView tvShareLicenseSummary;
+
+ @Inject
+ MediaLicenseContract.UserActionListener presenter;
+
+ private ArrayAdapter adapter;
+ private List licenses;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_media_license, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ButterKnife.bind(this, view);
+ init();
+ }
+
+ private void init() {
+ tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
+ callback.getTotalNumberOfSteps()));
+ initPresenter();
+ initLicenseSpinner();
+ presenter.getLicenses();
+ }
+
+ private void initPresenter() {
+ presenter.onAttachView(this);
+ }
+
+ /**
+ * Initialise the license spinner
+ */
+ private void initLicenseSpinner() {
+ adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item);
+ spinnerLicenseList.setAdapter(adapter);
+ spinnerLicenseList.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> adapterView, View view, int position,
+ long l) {
+ String licenseName = adapterView.getItemAtPosition(position).toString();
+ presenter.selectLicense(licenseName);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> adapterView) {
+ presenter.selectLicense(null);
+ }
+ });
+ }
+
+ @Override
+ public void setLicenses(List licenses) {
+ adapter.clear();
+ this.licenses = licenses;
+ adapter.addAll(this.licenses);
+ adapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void setSelectedLicense(String license) {
+ int position = licenses.indexOf(getString(Utils.licenseNameFor(license)));
+ // Check if position is valid
+ if (position < 0) {
+ Timber.d("Invalid position: %d. Using default licenses", position);
+ position = licenses.size() - 1;
+ } else {
+ Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license)));
+ }
+ spinnerLicenseList.setSelection(position);
+ }
+
+ @Override
+ public void updateLicenseSummary(String licenseSummary, int numberOfItems) {
+ String licenseHyperLink = "" +
+ getString(Utils.licenseNameFor(licenseSummary)) + "
";
+
+ setTextViewHTML(tvShareLicenseSummary, getResources()
+ .getQuantityString(R.plurals.share_license_summary, numberOfItems,
+ licenseHyperLink));
+ }
+
+ private void setTextViewHTML(TextView textView, String text) {
+ CharSequence sequence = Html.fromHtml(text);
+ SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence);
+ URLSpan[] urls = strBuilder.getSpans(0, sequence.length(), URLSpan.class);
+ for (URLSpan span : urls) {
+ makeLinkClickable(strBuilder, span);
+ }
+ textView.setText(strBuilder);
+ textView.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+
+ private void makeLinkClickable(SpannableStringBuilder strBuilder, final URLSpan span) {
+ int start = strBuilder.getSpanStart(span);
+ int end = strBuilder.getSpanEnd(span);
+ int flags = strBuilder.getSpanFlags(span);
+ ClickableSpan clickable = new ClickableSpan() {
+ public void onClick(View view) {
+ // Handle hyperlink click
+ String hyperLink = span.getURL();
+ launchBrowser(hyperLink);
+ }
+ };
+ strBuilder.setSpan(clickable, start, end, flags);
+ strBuilder.removeSpan(span);
+ }
+
+ private void launchBrowser(String hyperLink) {
+ Utils.handleWebUrl(getContext(), Uri.parse(hyperLink));
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ presenter.onDetachView();
+ //Free the adapter to avoid memory leaks
+ adapter=null;
+ }
+
+
+ @OnClick(R.id.btn_previous)
+ public void onPreviousButtonClicked() {
+ callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
+ }
+
+ @OnClick(R.id.btn_submit)
+ public void onSubmitButtonClicked() {
+ callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java
new file mode 100644
index 000000000..881f21369
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java
@@ -0,0 +1,75 @@
+package fr.free.nrw.commons.upload.license;
+
+import fr.free.nrw.commons.Utils;
+import fr.free.nrw.commons.repository.UploadRepository;
+import fr.free.nrw.commons.settings.Prefs;
+import fr.free.nrw.commons.upload.license.MediaLicenseContract.View;
+
+import java.lang.reflect.Proxy;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import timber.log.Timber;
+
+/**
+ * Added JavaDocs for MediaLicensePresenter
+ */
+public class MediaLicensePresenter implements MediaLicenseContract.UserActionListener {
+
+ private static final MediaLicenseContract.View DUMMY = (MediaLicenseContract.View) Proxy
+ .newProxyInstance(
+ MediaLicenseContract.View.class.getClassLoader(),
+ new Class[]{MediaLicenseContract.View.class},
+ (proxy, method, methodArgs) -> null);
+
+ private final UploadRepository repository;
+ private MediaLicenseContract.View view = DUMMY;
+
+ @Inject
+ public MediaLicensePresenter(UploadRepository uploadRepository) {
+ this.repository = uploadRepository;
+ }
+
+ @Override
+ public void onAttachView(View view) {
+ this.view = view;
+ }
+
+ @Override
+ public void onDetachView() {
+ this.view = DUMMY;
+ }
+
+ /**
+ * asks the repository for the available licenses, and informs the view on the same
+ */
+ @Override
+ public void getLicenses() {
+ List licenses = repository.getLicenses();
+ view.setLicenses(licenses);
+
+ String selectedLicense = repository.getValue(Prefs.DEFAULT_LICENSE,
+ Prefs.Licenses.CC_BY_SA_4);//CC_BY_SA_4 is the default one used by the commons web app
+ try {//I have to make sure that the stored default license was not one of the deprecated one's
+ Utils.licenseNameFor(selectedLicense);
+ } catch (IllegalStateException exception) {
+ Timber.e(exception.getMessage());
+ selectedLicense = Prefs.Licenses.CC_BY_SA_4;
+ repository.saveValue(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4);
+ }
+ view.setSelectedLicense(selectedLicense);
+
+ }
+
+ /**
+ * ask the repository to select a license for the current upload
+ *
+ * @param licenseName
+ */
+ @Override
+ public void selectLicense(String licenseName) {
+ repository.setSelectedLicense(licenseName);
+ view.updateLicenseSummary(repository.getSelectedLicense(), repository.getCount());
+ }
+}
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
new file mode 100644
index 000000000..0b589fc77
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java
@@ -0,0 +1,402 @@
+package fr.free.nrw.commons.upload.mediaDetails;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatButton;
+import androidx.appcompat.widget.AppCompatImageButton;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.github.chrisbanes.photoview.OnScaleChangedListener;
+import com.github.chrisbanes.photoview.PhotoView;
+import com.jakewharton.rxbinding2.widget.RxTextView;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import javax.inject.Inject;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.Utils;
+import fr.free.nrw.commons.filepicker.UploadableFile;
+import fr.free.nrw.commons.location.LatLng;
+import fr.free.nrw.commons.nearby.Place;
+import fr.free.nrw.commons.upload.Description;
+import fr.free.nrw.commons.upload.DescriptionsAdapter;
+import fr.free.nrw.commons.upload.SimilarImageDialogFragment;
+import fr.free.nrw.commons.upload.Title;
+import fr.free.nrw.commons.upload.UploadBaseFragment;
+import fr.free.nrw.commons.upload.UploadModel;
+import fr.free.nrw.commons.upload.UploadModel.UploadItem;
+import fr.free.nrw.commons.utils.DialogUtil;
+import fr.free.nrw.commons.utils.ImageUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
+import io.reactivex.disposables.Disposable;
+import timber.log.Timber;
+
+import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
+
+public class UploadMediaDetailFragment extends UploadBaseFragment implements
+ UploadMediaDetailsContract.View {
+
+ @BindView(R.id.tv_title)
+ TextView tvTitle;
+ @BindView(R.id.ib_map)
+ AppCompatImageButton ibMap;
+ @BindView(R.id.ib_expand_collapse)
+ AppCompatImageButton ibExpandCollapse;
+ @BindView(R.id.ll_container_media_detail)
+ LinearLayout llContainerMediaDetail;
+ @BindView(R.id.et_title)
+ EditText etTitle;
+ @BindView(R.id.rv_descriptions)
+ RecyclerView rvDescriptions;
+ @BindView(R.id.backgroundImage)
+ PhotoView photoViewBackgroundImage;
+ @BindView(R.id.btn_next)
+ AppCompatButton btnNext;
+ @BindView(R.id.btn_previous)
+ AppCompatButton btnPrevious;
+ private DescriptionsAdapter descriptionsAdapter;
+ @BindView(R.id.btn_copy_prev_title_desc)
+ AppCompatButton btnCopyPreviousTitleDesc;
+
+ private UploadModel.UploadItem uploadItem;
+ private List descriptions;
+
+ @Inject
+ UploadMediaDetailsContract.UserActionListener presenter;
+ private UploadableFile uploadableFile;
+ private String source;
+ private Place place;
+
+ private Title title;
+ private boolean isExpanded = true;
+
+ private UploadMediaDetailFragmentCallback callback;
+
+ public void setCallback(UploadMediaDetailFragmentCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ public void setImageTobeUploaded(UploadableFile uploadableFile, String source, Place place) {
+ this.uploadableFile = uploadableFile;
+ this.source = source;
+ this.place = place;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_upload_media_detail_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ButterKnife.bind(this, view);
+ init();
+ }
+
+ private void init() {
+ tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
+ callback.getTotalNumberOfSteps()));
+ title = new Title();
+ initRecyclerView();
+ initPresenter();
+ Disposable disposable = RxTextView.textChanges(etTitle)
+ .subscribe(text -> {
+ if (!TextUtils.isEmpty(text)) {
+ btnNext.setEnabled(true);
+ btnNext.setClickable(true);
+ btnNext.setAlpha(1.0f);
+ title.setTitleText(text.toString());
+ uploadItem.setTitle(title);
+ } else {
+ btnNext.setAlpha(0.5f);
+ btnNext.setEnabled(false);
+ btnNext.setClickable(false);
+ }
+ });
+ compositeDisposable.add(disposable);
+ presenter.receiveImage(uploadableFile, source, place);
+
+ if (callback.getIndexInViewFlipper(this) == 0) {
+ btnPrevious.setEnabled(false);
+ btnPrevious.setAlpha(0.5f);
+ } else {
+ btnPrevious.setEnabled(true);
+ btnPrevious.setAlpha(1.0f);
+ }
+
+ //If this is the first media, we have nothing to copy, lets not show the button
+ if (callback.getIndexInViewFlipper(this) == 0) {
+ btnCopyPreviousTitleDesc.setVisibility(View.GONE);
+ } else {
+ btnCopyPreviousTitleDesc.setVisibility(View.VISIBLE);
+ }
+
+ attachImageViewScaleChangeListener();
+
+ addEtTitleTouchListener();
+ }
+
+ /**
+ * Handles the drawable click listener for Edit Text
+ */
+ private void addEtTitleTouchListener() {
+ etTitle.setOnTouchListener((v, event) -> {
+ //2 is for drawable right
+ float twelveDpInPixels = convertDpToPixel(12, getContext());
+ if (event.getAction() == MotionEvent.ACTION_UP && etTitle.getCompoundDrawables()[2].getBounds().contains((int)(etTitle.getWidth()-(event.getX()+twelveDpInPixels)),(int)(event.getY()-twelveDpInPixels))){
+ showInfoAlert(R.string.media_detail_title,R.string.title_info);
+ return true;
+ }
+ return false;
+ });
+ }
+
+ /**
+ * converts dp to pixel
+ * @param dp
+ * @param context
+ * @return
+ */
+ private float convertDpToPixel(float dp, Context context) {
+ return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
+ }
+
+ /**
+ * Attaches the scale change listener to the image view
+ */
+ private void attachImageViewScaleChangeListener() {
+ photoViewBackgroundImage.setOnScaleChangeListener(
+ (scaleFactor, focusX, focusY) -> {
+ //Whenever the uses plays with the image, lets collapse the media detail container
+ expandCollapseLlMediaDetail(false);
+ });
+ }
+
+ /**
+ * attach the presenter with the view
+ */
+ private void initPresenter() {
+ presenter.onAttachView(this);
+ }
+
+ /**
+ * init the recycler veiw
+ */
+ private void initRecyclerView() {
+ descriptionsAdapter = new DescriptionsAdapter();
+ descriptionsAdapter.setCallback(this::showInfoAlert);
+ rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
+ rvDescriptions.setAdapter(descriptionsAdapter);
+ }
+
+ /**
+ * show dialog with info
+ * @param titleStringID
+ * @param messageStringId
+ */
+ private void showInfoAlert(int titleStringID, int messageStringId) {
+ DialogUtil.showAlertDialog(getActivity(), getString(titleStringID), getString(messageStringId), getString(android.R.string.ok), null, true);
+ }
+
+ @OnClick(R.id.btn_next)
+ public void onNextButtonClicked() {
+ uploadItem.setDescriptions(descriptionsAdapter.getDescriptions());
+ presenter.verifyImageQuality(uploadItem, true);
+ }
+
+ @OnClick(R.id.btn_previous)
+ public void onPreviousButtonClicked() {
+ callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
+ }
+
+ @OnClick(R.id.btn_add_description)
+ public void onButtonAddDescriptionClicked() {
+ Description description = new Description();
+ description.setManuallyAdded(true);//This was manually added by the user
+ descriptionsAdapter.addDescription(description);
+ }
+
+ @Override
+ public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) {
+ SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
+ newFragment.setCallback(new SimilarImageDialogFragment.Callback() {
+ @Override
+ public void onPositiveResponse() {
+ Timber.d("positive response from similar image fragment");
+ }
+
+ @Override
+ public void onNegativeResponse() {
+ Timber.d("negative response from similar image fragment");
+ }
+ });
+ Bundle args = new Bundle();
+ args.putString("originalImagePath", originalFilePath);
+ args.putString("possibleImagePath", possibleFilePath);
+ newFragment.setArguments(args);
+ newFragment.show(getChildFragmentManager(), "dialog");
+ }
+
+ @Override
+ public void onImageProcessed(UploadItem uploadItem, Place place) {
+ this.uploadItem = uploadItem;
+ if (uploadItem.getTitle() != null) {
+ etTitle.setText(uploadItem.getTitle().toString());
+ }
+
+ descriptions = uploadItem.getDescriptions();
+ photoViewBackgroundImage.setImageURI(uploadItem.getMediaUri());
+ setDescriptionsInAdapter(descriptions);
+ }
+
+ @Override
+ public void showProgress(boolean shouldShow) {
+ callback.showProgress(shouldShow);
+ }
+
+ @Override
+ public void onImageValidationSuccess() {
+ presenter.setUploadItem(callback.getIndexInViewFlipper(this), uploadItem);
+ callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
+ }
+
+ @Override
+ public void showMessage(int stringResourceId, int colorResourceId) {
+ ViewUtil.showLongToast(getContext(), stringResourceId);
+ }
+
+ @Override
+ public void showMessage(String message, int colorResourceId) {
+ ViewUtil.showLongToast(getContext(), message);
+ }
+
+ @Override
+ public void showDuplicatePicturePopup() {
+ String uploadTitleFormat = getString(R.string.upload_title_duplicate);
+ DialogUtil.showAlertDialog(getActivity(),
+ getString(R.string.warning),
+ String.format(Locale.getDefault(),
+ uploadTitleFormat,
+ uploadItem.getFileName()),
+ () -> {
+
+ },
+ () -> {
+ uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP);
+ onNextButtonClicked();
+ });
+ }
+
+ @Override
+ public void showBadImagePopup(Integer errorCode) {
+ String errorMessageForResult = getErrorMessageForResult(getContext(), errorCode);
+ if (!StringUtils.isBlank(errorMessageForResult)) {
+ DialogUtil.showAlertDialog(getActivity(),
+ getString(R.string.warning),
+ errorMessageForResult,
+ () -> deleteThisPicture(),
+ () -> {
+ uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP);
+ onNextButtonClicked();
+ });
+ }
+ //If the error message is null, we will probably not show anything
+ }
+
+ @Override public void showMapWithImageCoordinates(boolean shouldShow) {
+ ibMap.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void setTitleAndDescription(String title, List descriptions) {
+ etTitle.setText(title);
+ setDescriptionsInAdapter(descriptions);
+ }
+
+ private void deleteThisPicture() {
+ callback.deletePictureAtIndex(callback.getIndexInViewFlipper(this));
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ presenter.onDetachView();
+ }
+
+ @OnClick(R.id.rl_container_title)
+ public void onRlContainerTitleClicked() {
+ expandCollapseLlMediaDetail(!isExpanded);
+ }
+
+ /**
+ * show hide media detail based on
+ * @param shouldExpand
+ */
+ private void expandCollapseLlMediaDetail(boolean shouldExpand){
+ llContainerMediaDetail.setVisibility(shouldExpand ? View.VISIBLE : View.GONE);
+ isExpanded = !isExpanded;
+ ibExpandCollapse.setRotation(ibExpandCollapse.getRotation() + 180);
+ }
+
+ @OnClick(R.id.ib_map) public void onIbMapClicked() {
+ Utils.handleGeoCoordinates(getContext(),
+ new LatLng(uploadItem.getGpsCoords().getDecLatitude(),
+ uploadItem.getGpsCoords().getDecLongitude(), 0.0f));
+ }
+
+
+ public interface UploadMediaDetailFragmentCallback extends Callback {
+
+ void deletePictureAtIndex(int index);
+ }
+
+
+ @OnClick(R.id.btn_copy_prev_title_desc)
+ public void onButtonCopyPreviousTitleDesc(){
+ presenter.fetchPreviousTitleAndDescription(callback.getIndexInViewFlipper(this));
+ }
+
+ private void setDescriptionsInAdapter(List descriptions){
+ if(descriptions==null){
+ descriptions=new ArrayList<>();
+ }
+
+ if(descriptions.size()==0){
+ descriptions.add(new Description());
+ }
+
+ descriptionsAdapter.setItems(descriptions);
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java
new file mode 100644
index 000000000..9447000ab
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java
@@ -0,0 +1,52 @@
+package fr.free.nrw.commons.upload.mediaDetails;
+
+import java.util.List;
+
+import fr.free.nrw.commons.BasePresenter;
+import fr.free.nrw.commons.contributions.Contribution;
+import fr.free.nrw.commons.filepicker.UploadableFile;
+import fr.free.nrw.commons.nearby.Place;
+import fr.free.nrw.commons.upload.Description;
+import fr.free.nrw.commons.upload.SimilarImageInterface;
+import fr.free.nrw.commons.upload.Title;
+import fr.free.nrw.commons.upload.UploadModel.UploadItem;
+
+/**
+ * The contract with with UploadMediaDetails and its presenter would talk to each other
+ */
+public interface UploadMediaDetailsContract {
+
+ interface View extends SimilarImageInterface {
+
+ void onImageProcessed(UploadItem uploadItem, Place place);
+
+ void showProgress(boolean shouldShow);
+
+ void onImageValidationSuccess();
+
+ void showMessage(int stringResourceId, int colorResourceId);
+
+ void showMessage(String message, int colorResourceId);
+
+ void showDuplicatePicturePopup();
+
+ void showBadImagePopup(Integer errorCode);
+
+ void showMapWithImageCoordinates(boolean shouldShow);
+
+ void setTitleAndDescription(String title, List descriptions);
+ }
+
+ interface UserActionListener extends BasePresenter {
+
+ void receiveImage(UploadableFile uploadableFile, @Contribution.FileSource String source,
+ Place place);
+
+ void verifyImageQuality(UploadItem uploadItem, boolean validateTitle);
+
+ void setUploadItem(int index, UploadItem uploadItem);
+
+ void fetchPreviousTitleAndDescription(int indexInViewFlipper);
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java
new file mode 100644
index 000000000..3cccfe89d
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java
@@ -0,0 +1,194 @@
+package fr.free.nrw.commons.upload.mediaDetails;
+
+import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
+import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
+import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
+import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
+import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
+import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
+
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.filepicker.UploadableFile;
+import fr.free.nrw.commons.nearby.Place;
+import fr.free.nrw.commons.repository.UploadRepository;
+import fr.free.nrw.commons.upload.GPSExtractor;
+import fr.free.nrw.commons.upload.SimilarImageInterface;
+import fr.free.nrw.commons.upload.UploadModel.UploadItem;
+import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.UserActionListener;
+import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.View;
+import io.reactivex.Scheduler;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
+
+import java.lang.reflect.Proxy;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import timber.log.Timber;
+
+public class UploadMediaPresenter implements UserActionListener, SimilarImageInterface {
+
+ private static final UploadMediaDetailsContract.View DUMMY = (UploadMediaDetailsContract.View) Proxy
+ .newProxyInstance(
+ UploadMediaDetailsContract.View.class.getClassLoader(),
+ new Class[]{UploadMediaDetailsContract.View.class},
+ (proxy, method, methodArgs) -> null);
+
+ private final UploadRepository repository;
+ private UploadMediaDetailsContract.View view = DUMMY;
+
+ private CompositeDisposable compositeDisposable;
+
+ private Scheduler ioScheduler;
+ private Scheduler mainThreadScheduler;
+
+ @Inject
+ public UploadMediaPresenter(UploadRepository uploadRepository,
+ @Named(IO_THREAD) Scheduler ioScheduler,
+ @Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
+ this.repository = uploadRepository;
+ this.ioScheduler = ioScheduler;
+ this.mainThreadScheduler = mainThreadScheduler;
+ compositeDisposable = new CompositeDisposable();
+ }
+
+ @Override
+ public void onAttachView(View view) {
+ this.view = view;
+ }
+
+ @Override
+ public void onDetachView() {
+ this.view = DUMMY;
+ compositeDisposable.clear();
+ }
+
+ /**
+ * Receives the corresponding uploadable file, processes it and return the view with and uplaod item
+ *
+ * @param uploadableFile
+ * @param source
+ * @param place
+ */
+ @Override
+ public void receiveImage(UploadableFile uploadableFile, String source, Place place) {
+ view.showProgress(true);
+ Disposable uploadItemDisposable = repository
+ .preProcessImage(uploadableFile, place, source, this)
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .subscribe(uploadItem ->
+ {
+ view.onImageProcessed(uploadItem, place);
+ GPSExtractor gpsCoords = uploadItem.getGpsCoords();
+ view.showMapWithImageCoordinates((gpsCoords != null && gpsCoords.imageCoordsExists) ? true : false);
+ view.showProgress(false);
+ },
+ throwable -> Timber.e(throwable, "Error occurred in processing images"));
+ compositeDisposable.add(uploadItemDisposable);
+ }
+
+ /**
+ * asks the repository to verify image quality
+ *
+ * @param uploadItem
+ * @param validateTitle
+ */
+ @Override
+ public void verifyImageQuality(UploadItem uploadItem, boolean validateTitle) {
+ view.showProgress(true);
+ Disposable imageQualityDisposable = repository
+ .getImageQuality(uploadItem, true)
+ .subscribeOn(ioScheduler)
+ .observeOn(mainThreadScheduler)
+ .subscribe(imageResult -> {
+ view.showProgress(false);
+ handleImageResult(imageResult);
+ },
+ throwable -> {
+ view.showProgress(false);
+ view.showMessage("" + throwable.getLocalizedMessage(),
+ R.color.color_error);
+ Timber.e(throwable, "Error occurred while handling image");
+ });
+
+ compositeDisposable.add(imageQualityDisposable);
+ }
+
+ /**
+ * Adds the corresponding upload item to the repository
+ *
+ * @param index
+ * @param uploadItem
+ */
+ @Override
+ public void setUploadItem(int index, UploadItem uploadItem) {
+ repository.updateUploadItem(index, uploadItem);
+ }
+
+ /**
+ * Fetches and sets the title and desctiption of the previous item
+ *
+ * @param indexInViewFlipper
+ */
+ @Override
+ public void fetchPreviousTitleAndDescription(int indexInViewFlipper) {
+ UploadItem previousUploadItem = repository.getPreviousUploadItem(indexInViewFlipper);
+ if (null != previousUploadItem) {
+ view.setTitleAndDescription(previousUploadItem.getTitle().getTitleText(), previousUploadItem.getDescriptions());
+ } else {
+ view.showMessage(R.string.previous_image_title_description_not_found, R.color.color_error);
+ }
+ }
+
+ /**
+ * handles image quality verifications
+ *
+ * @param imageResult
+ */
+ public void handleImageResult(Integer imageResult) {
+ if (imageResult == IMAGE_KEEP || imageResult == IMAGE_OK) {
+ view.onImageValidationSuccess();
+ } else {
+ handleBadImage(imageResult);
+ }
+ }
+
+ /**
+ * Handle images, say empty title, duplicate file name, bad picture(in all other cases)
+ *
+ * @param errorCode
+ */
+ private void handleBadImage(Integer errorCode) {
+ Timber.d("Handle bad picture with error code %d", errorCode);
+ if (errorCode
+ >= 8) { // If location of image and nearby does not match, then set shared preferences to disable wikidata edits
+ repository.saveValue("Picture_Has_Correct_Location", false);
+ }
+
+ switch (errorCode) {
+ case EMPTY_TITLE:
+ Timber.d("Title is empty. Showing toast");
+ view.showMessage(R.string.add_title_toast, R.color.color_error);
+ break;
+ case FILE_NAME_EXISTS:
+ Timber.d("Trying to show duplicate picture popup");
+ view.showDuplicatePicturePopup();
+ break;
+ default:
+ view.showBadImagePopup(errorCode);
+ }
+ }
+
+ /**
+ * notifies the user that a similar image exists
+ *
+ * @param originalFilePath
+ * @param possibleFilePath
+ */
+ @Override
+ public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) {
+ view.showSimilarImageFragment(originalFilePath, possibleFilePath);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java
index 53d129fb3..e9765551c 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java
@@ -140,4 +140,31 @@ public class DialogUtil {
showSafely(activity, dialog);
}
+
+ /**
+ * show a dialog with just a positive button
+ * @param activity
+ * @param title
+ * @param message
+ * @param positiveButtonText
+ * @param positiveButtonClick
+ * @param cancellable
+ */
+ public static void showAlertDialog(Activity activity, String title, String message, String positiveButtonText, final Runnable positiveButtonClick, boolean cancellable) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(title);
+ builder.setMessage(message);
+ builder.setCancelable(cancellable);
+
+ builder.setPositiveButton(positiveButtonText, (dialogInterface, i) -> {
+ dialogInterface.dismiss();
+ if (positiveButtonClick != null) {
+ positiveButtonClick.run();
+ }
+ });
+
+ AlertDialog dialog = builder.create();
+ showSafely(activity, dialog);
+ }
+
}
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 4b51921ac..eac1f7cde 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
@@ -79,7 +79,7 @@ public class WikidataEditService {
Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId);
Observable.fromCallable(() -> {
String propertyValue = getFileName(fileName);
- return mediaWikiApi.wikidatCreateClaim(wikidataEntityId, "P18", "value", propertyValue);
+ return mediaWikiApi.wikidataCreateClaim(wikidataEntityId, "P18", "value", propertyValue);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
diff --git a/app/src/main/res/drawable/drawable_thumbnail_image.xml b/app/src/main/res/drawable/drawable_thumbnail_image.xml
new file mode 100644
index 000000000..b406e8938
--- /dev/null
+++ b/app/src/main/res/drawable/drawable_thumbnail_image.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/thumbnail_not_selected.xml b/app/src/main/res/drawable/thumbnail_not_selected.xml
new file mode 100644
index 000000000..8ead4b377
--- /dev/null
+++ b/app/src/main/res/drawable/thumbnail_not_selected.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/thumbnail_selected.xml b/app/src/main/res/drawable/thumbnail_selected.xml
new file mode 100644
index 000000000..ac6ec9335
--- /dev/null
+++ b/app/src/main/res/drawable/thumbnail_selected.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload.xml b/app/src/main/res/layout/activity_upload.xml
index 3987a53d3..7822ce593 100644
--- a/app/src/main/res/layout/activity_upload.xml
+++ b/app/src/main/res/layout/activity_upload.xml
@@ -1,34 +1,79 @@
-
-
+
+
+
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="@dimen/standard_gap"
+ >
-
+
+
+
-
+
-
+
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_bottom_card.xml b/app/src/main/res/layout/activity_upload_bottom_card.xml
deleted file mode 100644
index 3afe769df..000000000
--- a/app/src/main/res/layout/activity_upload_bottom_card.xml
+++ /dev/null
@@ -1,199 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_upload_categories.xml b/app/src/main/res/layout/activity_upload_categories.xml
deleted file mode 100644
index 61f35cc33..000000000
--- a/app/src/main/res/layout/activity_upload_categories.xml
+++ /dev/null
@@ -1,127 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_license.xml b/app/src/main/res/layout/activity_upload_license.xml
deleted file mode 100644
index 0f5e94d31..000000000
--- a/app/src/main/res/layout/activity_upload_license.xml
+++ /dev/null
@@ -1,116 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_please_wait.xml b/app/src/main/res/layout/activity_upload_please_wait.xml
deleted file mode 100644
index 008f37c83..000000000
--- a/app/src/main/res/layout/activity_upload_please_wait.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_media_license.xml b/app/src/main/res/layout/fragment_media_license.xml
new file mode 100644
index 000000000..4b46ebb6f
--- /dev/null
+++ b/app/src/main/res/layout/fragment_media_license.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_upload_media_detail_fragment.xml b/app/src/main/res/layout/fragment_upload_media_detail_fragment.xml
new file mode 100644
index 000000000..84003afa6
--- /dev/null
+++ b/app/src/main/res/layout/fragment_upload_media_detail_fragment.xml
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_upload_thumbnail.xml b/app/src/main/res/layout/item_upload_thumbnail.xml
index 6a91afb64..9b8e8ee74 100644
--- a/app/src/main/res/layout/item_upload_thumbnail.xml
+++ b/app/src/main/res/layout/item_upload_thumbnail.xml
@@ -1,37 +1,28 @@
-
+
-
+
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
diff --git a/app/src/main/res/layout/row_item_description.xml b/app/src/main/res/layout/row_item_description.xml
index c2f520ca7..32136b29d 100644
--- a/app/src/main/res/layout/row_item_description.xml
+++ b/app/src/main/res/layout/row_item_description.xml
@@ -19,7 +19,7 @@
android:layout_height="wrap_content"
android:layout_weight="8">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 8185aab5b..873a3101d 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -179,6 +179,7 @@
طلب إذن التخزين
صلاحية مطلوبة: قراءة وحدة التخزين الخارجية، لا يمكن للتطبيق الوصول إلى معرض الصور الخاص بك بدونها.
صلاحية مطلوبة: اكتب وحدة التخزين الخارجية، لا يمكن للتطبيق الوصول إلى معرض الصور/الكاميرا الخاصة بك بدونها.
+ جارٍ طلب إذن الموقع
صلاحية اختيارية: احصل على الموقع الحالي لاقتراحات التصنيفات
موافق
الأماكن القريبة
@@ -530,4 +531,5 @@
ارفع الصور لويكيميديا كومنز على هاتفك قم بتنزيل تطبيق كومنز: %1$s
مشاركة التطبيق عبر...
معلومات الصورة
+ لماذا يجب حذف %1$s؟
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 5371fbaa8..86a140485 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -215,6 +215,7 @@
Персонализираното авторско име, което ще се използва вместо потребителското ви име при качване
Известия (архивирани)
Списък
+ Изпращане
Америка
Европа
Африка
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index b89520ef5..95d2f7ccc 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -15,6 +15,7 @@
Udseende
Generelt
Tilbagemelding
+ Privatliv
Sted
Commons
•
@@ -370,5 +371,8 @@
Der opstod en fejl under udvælgelse af billeder
Vent venligst…
Spring over dette billede
+ Ophavsret
+ Kameramodel
+ Serienumre
Del app via...
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 000091274..47760ee8a 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -339,6 +339,7 @@
آیا این تصویر برای بارگذاری مناسب است؟
پرسش
نتیجه
+ شما %1$s پاسخ درست دادید. آفرین!
یکی از دو گزینه را انتخاب کنید تا به سوال پاسخ دهید
جلسه ورود به سیستم منقضی شد، لطفا دوباره وارد سیستم شوید.
کویز خود را با دوستان خود به اشتراک بگذارید.
@@ -383,6 +384,11 @@
بعدی
قبلی
ارسال
+ پروندهای با نام %1$s وجود دارد. آیا اطمینان دارید که میخواهید ادامه دهید؟
+
+ - %1$d بارگذاری
+ - %1$d بارگذاری
+
نشانکها
نشانکها
تصویرها
@@ -396,6 +402,7 @@
فهمیدم که این برای حریم خصوصی من بد است.
من تغییر عقیده دادم، نمیخواهم دیگر برای همه قابلمشاهده باشد.
با پوزش، این تصویر برای یک دانشنامه مناسب نیست
+ بارگذاریشده توسط خودم در %1$s؛ استفادهشده در %2$d مقاله.
به کامانز خوش آمدید!\n\nاولین فایلتان را با فشردن کلید اضافه بارگذاری کنید.
در سراسر جهان
آمریکا
@@ -423,6 +430,7 @@
به کاربر در صفحه بحثش خبر بده
مطمئن نیستم
ارسال تشکر: موفق
+ تشکر با موفقیت برای %1$s فرستاده شد
تلاش برای فرستادن تشکر شکست خورد %1$s
ارسال تشکر: ناموفق
ارسال تشکر
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index d191a98ad..850d1545b 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -189,6 +189,7 @@
Demande d\'autorisation d\'accès au stockage
Autorisation nécessaire : Lire un stockage externe. L’application ne peut pas accéder à votre galerie sans cela.
Permission obligatoire : Écriture sur stockage externe. L’application ne peut pas accéder à votre appareil photo/galerie sans cela.
+ Demande d\'autorisation d\'accès au stockage
Autorisation facultative : Obtenir l’emplacement actuel pour des suggestions de catégorie
OK
Endroits à proximité
@@ -502,8 +503,8 @@
Semble correct
Oui, pourquoi pas
Image suivante
- Cliquer sur ce bouton vous fournira une autre image récemment téléversée de Wikimédia Communs.
- Vous pouvez revoir les images et améliorer la qualité de Wikimédia Communs.\n Les quatre paramètres à revoir sont : \n - Cette image est-elle à propos ? \n - Cette image respecte-t-elle les règles de droit d’auteur ? \n - Cette image est-elle bien catégorisée ? \n - Si tout va bien, vous pouvez aussi remercier le contributeur.
+ Cliquer sur ce bouton vous fournira une autre image récemment téléversée de Wikimédia Commons.
+ Vous pouvez revoir les images et améliorer la qualité de Wikimédia Commons.\n Les quatre paramètres à revoir sont : \n - Cette image est-elle à propos ? \n - Cette image respecte-t-elle les règles de droit d’auteur ? \n - Cette image est-elle bien catégorisée ? \n - Si tout va bien, vous pouvez aussi remercier le contributeur.
- Réception de contenu partagé. Le traitement de l\'image peut prendre un certain temps en fonction de la taille de l\'image et de votre matériel
- Réception de contenu partagé. Le traitement des images peut prendre un certain temps en fonction de la taille des images et de votre matériel
@@ -519,7 +520,7 @@
Erreur lors de la sélection des images
Choisir les images à téléverser
Veuillez patienter…
- Les images en vedette sont des images de photographes et d’illustrateurs très doués que la communauté de Wikimédia Communs a choisi comme étant de la meilleure qualité pour le site.
+ Les images en vedette sont des images de photographes et d’illustrateurs très doués que la communauté de Wikimédia Commons a choisi comme étant de la meilleure qualité pour le site.
Les images téléversées par les lieux de proximité sont les images téléversées par la découverte des lieux sur la carte.
Cette fonctionalité permet aux contributeurs d\'envoyer une notification de remerciement aux utilisateurs qui font des modifications utiles – en utilisant un petit lien de remerciement sur la page historique ou sur celle du diff.
Copier le titre et la description précédente
@@ -529,7 +530,7 @@
SAUTER CETTE IMAGE
Échec du téléchargement ! Nous ne pouvons pas télécharger le fichier sans droit de stockage externe.
Gérer les balises EXIF
- Sélectionner quelles balises EXIF à conserver dans les téléchargements
+ Sélectionner quelles balises EXIF à conserver dans les téléversements
Auteur
Droits d’auteur
Emplacement
@@ -537,7 +538,8 @@
Modèle de lentille
Numéros de série
Logiciel
- Téléverser des photos vers Wikimédia Communs, sur votre téléphone Téléchargez l’application Communs : %1$s
+ Téléverser des photos vers Wikimédia Commons, sur votre téléphone Téléchargez l’application Commons : %1$s
Partager l’application via…
Informations de l’image
+ Pourquoi %1$s devrait-il être supprimé ?
diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml
index 6e6272fad..67aae5ee2 100644
--- a/app/src/main/res/values-is/strings.xml
+++ b/app/src/main/res/values-is/strings.xml
@@ -9,6 +9,7 @@
Útlit
Almennt
Umsagnir
+ Persónuvernd
Staðsetning
Commons
• \
@@ -167,6 +168,7 @@
Biður um aðgang að geymslurými
Nauðsynlegar heimildir: Lesa ytri gagnageymslu. Forritið fær ekki aðgang að myndasafni ekki án þessa.
Nauðsynlegar heimildir: Skrifa í ytri gagnageymslu. Forritið nær ekki sambandi við myndavél/myndasafn ekki án þessa.
+ Biður um aðgang að staðsetningu
Nauðsynlegar heimildir: Lesa núverandi staðsetningu til að geta stungið upp á flokkum
Í lagi
Staðir í nágrenninu
@@ -430,6 +432,8 @@
Sjá yfirstandandi herferðir
Lokið
Ekki viss
+ Senda þakkarboð
+ Sendi þakkarboð
Er þetta rétt flokkað?
Kemur þetta umfjöllunarefninu við?
Það brýtur á móti höfundarrétti því það er
@@ -451,4 +455,13 @@
Veldu myndir til að senda inn
Bíddu aðeins…
SLEPPA ÞESSARI MYND
+ Sýsla með EXIF-merki
+ Höfundur
+ Höfundarréttur
+ Staðsetning
+ Tegund myndavélar
+ Tegund linsu
+ Raðnúmer
+ Hugbúnaður
+ Upplýsingar í mynd
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 1228946f3..32509e837 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -21,6 +21,7 @@
Aspetto
Generale
Commenti
+ Privacy
Posizione
Commons
•
@@ -470,6 +471,10 @@
Clicca per riusare il titolo e la descrizione dell\'immagine precedente e adattarli all\'immagine attuale.
SALTA QUESTA IMMAGINE
Autore
+ Modello fotocamera
+ Numeri seriali
+ Software
Condividi applicazione tramite...
Informazioni sull\'immagine
+ Perché %1$s dovrebbe essere cancellato?
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 9eaf98ffa..664d1ff72 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -176,6 +176,7 @@
기억 장치 권한 요청 중
권한 필요: 외부 저장소 읽기. 이것이 없으면 앱은 갤러리에 접근할 수 없습니다.
권한 필요: 외부 저장소 쓰기. 이것이 없으면 앱은 카메라에 접근할 수 없습니다.
+ 위치 권한 요청 중
선택적 권한: 분류 추천을 위해 현재 위치 정보를 가져옵니다.
확인
근처의 장소
@@ -458,6 +459,7 @@
저작권
위치
카메라 모델
+ 렌즈 모델
일련 번호
소프트웨어
앱 공유...
diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml
index 1a7adaaa6..dc1a1c2b2 100644
--- a/app/src/main/res/values-lb/strings.xml
+++ b/app/src/main/res/values-lb/strings.xml
@@ -338,4 +338,9 @@
Sicht Biller eraus fir eropzelueden
Waart w.e.g. ...
DËST BILD IWWERWSPRANGEN
+ Auteur
+ Copyright
+ Plaz
+ Seriennummeren
+ Software
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index 27cfbeda8..516b42219 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -169,6 +169,7 @@
Се бара дозвола за складирање
Потребна дозвола: Треба да се прочита од надворешен склад. Прилогот без ова нема пристап до вашата галерија.
Потребна дозвола: Треба да се запише на надворешен склад. Прилогот без ова нема пристап до вашата камера/галерија.
+ Се бара дозвола за утврдување на местоположбата
Дозвола по желба: Утврдување на тековната местоположба за предлагање категории
ОК
Околни места
@@ -520,4 +521,5 @@
Подигајте слики на Ризницата од телефон. Преземете го прилогот на Ризницата: %1$s
Сподели преку...
Инфо за сликата
+ Зошто сметате дека %1$s треба да се избрише?
diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml
index f6e5cef4a..5cc34d2cf 100644
--- a/app/src/main/res/values-my/strings.xml
+++ b/app/src/main/res/values-my/strings.xml
@@ -9,6 +9,7 @@
ပုံပန်းသွင်ပြင်
အထွေထွေ
အကြံပေးရန်
+ ကိုယ်ရေးမူဝါဒ
နေရာ
ကွန်မွန်းစ်
•
@@ -129,6 +130,7 @@
ဆွေးနွေးချက် မရှိပါ
အမည်မသိရသော လိုင်စင်
ပြန်လည်ဆန်းသစ်ရန်
+ တည်နေရာ ခွင့်ပြုချက် တောင်းဆိုနေသည်
အိုကေ
အနီးအနား နေရာများ
အနီးအနား နေရာများ မတွေ့ပါ
@@ -150,7 +152,7 @@
မကိုက်ညီသော ထည့်သွင်းမှု
၅၀၀ ထက်ပို၍ မပြသနိုင်ပါ
ကိုက်ညီသောနံပါတ်တစ်ခု ရိုက်ထည့်ပါ
- လတ်တလော အပ်ပလုတ်ကန့်သတ်ချက် သတ်မှတ်ရန်
+ လတ်တလော အပ်ပလုတ်ကန့်သတ်ချက်
အမှန်တကယ် ထွက်သွားလိုပါသလား
ကွန်မွန်းစ် လိုဂို
ကွန်မွန်းစ် ဝဘ်ဆိုဒ်
@@ -276,9 +278,24 @@
အာဖရိက
အာရှ
ပစိဖိတ်
+ ဟုတ်ကဲ့ ထည့်သွင်းမည်
+ ဟင်းအင်း၊ ပြန်သွားမည်
ဤဧရိယာကို ရှာဖွေပါ
ခွင့်ပြုချက် တောင်းခံရန်
+ နောက်တခါ ထပ်မမေးပါနှင့်
ပြီးပြီ
မသေချာပါ
+ ကျေးဇူးတင်မှု ပို့နေသည်
+ ကျေးဇူးတင်မှု ပို့နေသည်
+ %1$ အတွက် ကျေးဇူးတင်မှု ပို့နေသည်
+ နောက်ရုပ်ပုံ
+ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ
ကျေးဇူးပြု၍ ခဏစောင့်ပါ...
+ ဖန်တီးသူ
+ မူပိုင်ခွင့်
+ တည်နေရာ
+ ကင်မရာ မော်ဒယ်
+ ဆော့ဝဲလ်
+ ရုပ်ပုံ အချက်အလက်
+ %1$ ဟာ ဘာကြောင့် ဖျက်သင့်သလဲ?
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 0df25a964..2d1831e72 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -181,6 +181,7 @@
Solicitando permissão de armazenamento
Permissão necessária: leia o armazenamento externo. App não pode acessar sua galeria sem isso.
Permissão necessária: escreva o armazenamento externo. App não pode acessar sua câmera/galeria sem isso.
+ Autorização para identificar localização
Permissão opcional: Obter a localização atual de sugestões de categoria
OK
Lugares próximos
@@ -532,4 +533,5 @@
Faça o carregamento de fotos para o Wikimedia Commons no seu telefone ou baixe o aplicativo Commons: %1$s
Compartilhar aplicativo via...
Informação da imagem
+ Por que %1$s deve ser excluído?
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 477d9d333..96958bcec 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -23,6 +23,7 @@
Aparência
Geral
Comentários
+ Privacidade
Localização
Commons
•
@@ -181,6 +182,7 @@
A pedir permissão de armazenamento
Permissão necessária: Ler a armazenagem externa. A aplicação não pode aceder à sua galeria sem isto.
Permissão necessária: Escrever na armazenagem externa. A aplicação não pode aceder à sua câmara/galeria sem isto.
+ Autorização para identificar localização
Permissão opcional: Obter a localização atual para sugestões de categoria
OK
Locais Próximos
@@ -520,6 +522,15 @@
Exemplos de imagens que não devem ser carregadas
SALTAR ESTA IMAGEM
O descarregamento falhou! Não podemos descarregar o ficheiro sem permissão de armazenagem externa.
+ Gerir etiquetas EXIF
+ Selecionar as etiquetas EXIF a manter nos carregamentos
+ Autor
+ Direitos de autor
+ Localização
+ Modelo da câmara
+ Modelo da lente
+ Números de série
+ \'\'Software\'\'
Carregar fotografias na wiki Wikimedia Commons, do seu telemóvel Descarregar a aplicação Commons: %1$s
Partilhar aplicação por...
Informação da imagem
diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml
index e2cec15dc..cbfe2eab6 100644
--- a/app/src/main/res/values-qq/strings.xml
+++ b/app/src/main/res/values-qq/strings.xml
@@ -156,4 +156,6 @@
{{Identical|Submit}}
\"Send log file\" is {{msg-wm|Commons-android-strings-send log file}}.
{{Identical|Done}}
+ {{Identical|Author}}
+ {{Identical|Location}}
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 875ab4882..5b48f71ae 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -373,7 +373,7 @@
Avem nevoie de permisiunea dvs. pentru a accesa spațiul de stocare extern al dispozitivului dvs. pentru a încărca imagini.
Nu veți vedea cel mai apropiat loc care are nevoie de imagini. Cu toate acestea, puteți reactiva această notificare în Setări, dacă doriți.
Pasul %1$d din %2$d
- Imaginea% %1$d în set
+ Imaginea %1$d în set
Următor
Precedent
Trimite
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 4ec460d30..e05305690 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -187,6 +187,7 @@
Запрос разрешения по использованию внешнего носителя
Требуемые разрешения: чтение с внешнего носителя. Приложение не сможет получить доступ к вашей галерее без этого разрешения.
Требуемые разрешения: запись на внешнее хранилище. Приложение не сможет получить доступ к галерее/камере без этого разрешения.
+ Запрос на определение местоположения
Необязательное разрешение: получение текущего местоположения для предложения категорий
OK
Места поблизости
@@ -539,4 +540,5 @@
Чтобы загружать фото на Викисклад (Wikimedia Commons), скачайте одноимённое приложение «Викисклад» (Commons): %1$s
Поделиться приложением с помощью...
Информация об изображении
+ Почему %1$s должно быть удалено?
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index fe80d2520..64003204b 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -174,6 +174,7 @@
Begär lagringsbehörighet
Nödvändig behörighet: Läs extern lagring. Appen kan inte komma åt ditt galleri utan detta.
Nödvändig behörighet: Skriv till extern lagring. Appen kan inte komma åt din kamera/galleri utan detta.
+ Begär platsbehörighet
Valfri behörighet: Hämta aktuell plats för kategoriförslag
OK
Platser i närheten
@@ -525,4 +526,5 @@
Ladda upp foton till Wikimedia Commons på din telefon Ladda ned Commons-appen: %1$s
Dela appen via...
Bildinfo
+ Varför bör %1$s raderas?
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index f03d043d0..f6c716434 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -186,6 +186,7 @@
Запит дозволу на зберігання
Обов\'язковий дозвіл: читання зовнішньої пам\'яті. Без цього дозволу програма не зможе отримати доступ до вашої галереї.
Обов\'язковий дозвіл: записування на зовнішнє сховище. Програма не зможе отримати доступ до камери/галереї без цього дозволу.
+ Запит на визначення місцезнаходження
Додатковий дозвіл: отримувати поточне розташування для підказок категорій
Гаразд
Місця поблизу
@@ -539,4 +540,5 @@
Вивантажуйте фото у Вікісховище зі свого телефона. Завантажте застосунок: %1$s
Поділитися програмкою через…
Інформація про зображення
+ Чому %1$s має бути видалено?
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 66b74b66d..b59fabad6 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -44,7 +44,7 @@
輕觸來檢視您上傳的項目
開始上傳%1$s
正在上傳%1$s
- 即將完成上傳 %1$s
+ 即將完成上傳%1$s
上傳%1$s失敗
輕觸檢視
@@ -178,6 +178,7 @@
請求存儲裝置權限
必要權限:讀取外部存儲裝置。否則應用程式無法存取您的圖庫。
必要權限:寫入外部存儲裝置。否則應用程式無法取用您的相機/圖庫。
+ 請求位置權限
可有可無的權限:獲取目前的地理位置,以用於分類建議
好
附近地點
@@ -529,4 +530,5 @@
在您的手機上更新照片到維基共享資源,下載共享資源應用程式:%1$s
分享應用程式透過…
圖片資訊
+ 為何應刪除%1$s?
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index a580effd8..1c6f36aee 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -67,6 +67,7 @@
#000000
#FF0000
+ #FF0000
#B22222
#006400
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b460f358e..0c6ba28f0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -409,7 +409,7 @@
Next
Previous
Submit
- A file with the file name %1$s exists. Are you sure you want to proceed?
+ A file with the file name %1$s exists. Are you sure you want to proceed?
No compatible map application could be found on your device. Please install a map application to use this feature.
- %1$d Upload
@@ -554,4 +554,8 @@ Upload your first media by tapping on the add button.
Upload photos to Wikimedia Commons on your phone Download the Commons app: %1$s
Share app via...
Image Info
+ No Categories found
+ Cancelled Upload
+ There is no data for previous image\'s title or description
+ Why should %1$s be deleted?
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt
new file mode 100644
index 000000000..eae8defc5
--- /dev/null
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt
@@ -0,0 +1,80 @@
+package fr.free.nrw.commons.upload
+
+import com.nhaarman.mockito_kotlin.verify
+import fr.free.nrw.commons.category.CategoryItem
+import fr.free.nrw.commons.repository.UploadRepository
+import fr.free.nrw.commons.upload.categories.CategoriesContract
+import fr.free.nrw.commons.upload.categories.CategoriesPresenter
+import io.reactivex.Observable
+import io.reactivex.schedulers.TestScheduler
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+/**
+ * The class contains unit test cases for CategoriesPresenter
+ */
+class CategoriesPresenterTest {
+ @Mock
+ internal var repository: UploadRepository? = null
+ @Mock
+ internal var view: CategoriesContract.View? = null
+
+ var categoriesPresenter: CategoriesPresenter? = null
+
+ var testScheduler: TestScheduler? = null
+
+ val categoryItems: ArrayList = ArrayList()
+
+ @Mock
+ lateinit var categoryItem: CategoryItem
+
+ var testObservable: Observable? = null
+
+ private val imageTitleList = ArrayList()
+
+ /**
+ * initial setup
+ */
+ @Before
+ @Throws(Exception::class)
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testScheduler = TestScheduler()
+ categoryItems.add(categoryItem)
+ testObservable = Observable.just(categoryItem)
+ categoriesPresenter = CategoriesPresenter(repository, testScheduler, testScheduler)
+ categoriesPresenter?.onAttachView(view)
+ }
+
+ /**
+ * unit test case for method CategoriesPresenter.searchForCategories
+ */
+ @Test
+ fun searchForCategoriesTest() {
+ Mockito.`when`(repository?.sortBySimilarity(ArgumentMatchers.anyString())).thenReturn(Comparator { _, _ -> 1 })
+ Mockito.`when`(repository?.selectedCategories).thenReturn(categoryItems)
+ Mockito.`when`(repository?.searchAll(ArgumentMatchers.anyString(), ArgumentMatchers.anyList())).thenReturn(Observable.empty())
+ categoriesPresenter?.searchForCategories("test")
+ verify(view)?.showProgress(true)
+ verify(view)?.showError(null)
+ verify(view)?.setCategories(null)
+ testScheduler?.triggerActions()
+ verify(view)?.setCategories(categoryItems)
+ verify(view)?.showProgress(false)
+ }
+
+ /**
+ * unit test for method CategoriesPresenter.verifyCategories
+ */
+ @Test
+ fun verifyCategoriesTest() {
+ Mockito.`when`(repository?.selectedCategories).thenReturn(categoryItems)
+ categoriesPresenter?.verifyCategories()
+ verify(repository)?.setSelectedCategories(ArgumentMatchers.anyList())
+ verify(view)?.goToNextScreen()
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/MediaLicensePresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/MediaLicensePresenterTest.kt
new file mode 100644
index 000000000..6d64f6310
--- /dev/null
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/MediaLicensePresenterTest.kt
@@ -0,0 +1,66 @@
+package fr.free.nrw.commons.upload
+
+import com.nhaarman.mockito_kotlin.verify
+import fr.free.nrw.commons.Utils
+import fr.free.nrw.commons.repository.UploadRepository
+import fr.free.nrw.commons.upload.license.MediaLicenseContract
+import fr.free.nrw.commons.upload.license.MediaLicensePresenter
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.InjectMocks
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.powermock.api.mockito.PowerMockito
+import org.powermock.core.classloader.annotations.PrepareForTest
+import org.powermock.modules.junit4.PowerMockRunner
+
+/**
+ * The class contains unit test cases for MediaLicensePresenter
+ */
+
+@RunWith(PowerMockRunner::class)
+@PrepareForTest(Utils::class)
+class MediaLicensePresenterTest {
+ @Mock
+ internal var repository: UploadRepository? = null
+
+ @Mock
+ internal var view: MediaLicenseContract.View? = null
+
+ @InjectMocks
+ var mediaLicensePresenter: MediaLicensePresenter? = null
+
+ /**
+ * initial setup test environemnt
+ */
+ @Before
+ @Throws(Exception::class)
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ mediaLicensePresenter?.onAttachView(view)
+ PowerMockito.mockStatic(Utils::class.java)
+ PowerMockito.`when`(Utils.licenseNameFor(ArgumentMatchers.anyString())).thenReturn(1)
+ }
+
+
+ /**
+ * unit test case for method MediaLicensePresenter.getLicense
+ */
+ @Test
+ fun getLicenseTest() {
+ mediaLicensePresenter?.getLicenses()
+ verify(view)?.setLicenses(ArgumentMatchers.anyList())
+ verify(view)?.setSelectedLicense(ArgumentMatchers.any())
+ }
+
+ /**
+ * unit test case for method MediaLicensePresenter.selectLicense
+ */
+ @Test
+ fun selectLicenseTest() {
+ mediaLicensePresenter?.selectLicense(ArgumentMatchers.anyString())
+ verify(view)?.updateLicenseSummary(ArgumentMatchers.any(), ArgumentMatchers.anyInt())
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt
new file mode 100644
index 000000000..188785757
--- /dev/null
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt
@@ -0,0 +1,110 @@
+package fr.free.nrw.commons.upload
+
+import fr.free.nrw.commons.filepicker.UploadableFile
+import fr.free.nrw.commons.nearby.Place
+import fr.free.nrw.commons.repository.UploadRepository
+import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract
+import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter
+import fr.free.nrw.commons.utils.ImageUtils.*
+import io.reactivex.Observable
+import io.reactivex.Single
+import io.reactivex.schedulers.TestScheduler
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+
+/**
+ * The class contains unit test cases for UploadMediaPresenter
+ */
+class UploadMediaPresenterTest {
+ @Mock
+ internal var repository: UploadRepository? = null
+ @Mock
+ internal var view: UploadMediaDetailsContract.View? = null
+
+ private var uploadMediaPresenter: UploadMediaPresenter? = null
+
+ @Mock
+ private var uploadableFile: UploadableFile? = null
+
+ @Mock
+ private var place: Place? = null
+
+ @Mock
+ private var uploadItem: UploadModel.UploadItem? = null
+
+ private var testObservableUploadItem: Observable? = null
+ private var testSingleImageResult: Single? = null
+
+ private var testScheduler: TestScheduler? = null
+
+ /**
+ * initial setup unit test environment
+ */
+ @Before
+ @Throws(Exception::class)
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testObservableUploadItem = Observable.just(uploadItem)
+ testSingleImageResult = Single.just(1)
+ testScheduler = TestScheduler()
+ uploadMediaPresenter = UploadMediaPresenter(repository, testScheduler, testScheduler)
+ uploadMediaPresenter?.onAttachView(view)
+ }
+
+ /**
+ * unit test for method UploadMediaPresenter.receiveImage
+ */
+ @Test
+ fun receiveImageTest() {
+ Mockito.`when`(repository?.preProcessImage(ArgumentMatchers.any(UploadableFile::class.java), ArgumentMatchers.any(Place::class.java), ArgumentMatchers.anyString(), ArgumentMatchers.any(UploadMediaPresenter::class.java))).thenReturn(testObservableUploadItem)
+ uploadMediaPresenter?.receiveImage(uploadableFile, ArgumentMatchers.anyString(), place)
+ verify(view)?.showProgress(true)
+ testScheduler?.triggerActions()
+ verify(view)?.onImageProcessed(ArgumentMatchers.any(UploadModel.UploadItem::class.java), ArgumentMatchers.any(Place::class.java))
+ verify(view)?.showProgress(false)
+ }
+
+ /**
+ * unit test for method UploadMediaPresenter.verifyImageQuality
+ */
+ @Test
+ fun verifyImageQualityTest() {
+ Mockito.`when`(repository?.getImageQuality(ArgumentMatchers.any(UploadModel.UploadItem::class.java), ArgumentMatchers.any(Boolean::class.java))).thenReturn(testSingleImageResult)
+ Mockito.`when`(uploadItem?.imageQuality).thenReturn(ArgumentMatchers.anyInt())
+ uploadMediaPresenter?.verifyImageQuality(uploadItem, true)
+ verify(view)?.showProgress(true)
+ testScheduler?.triggerActions()
+ verify(view)?.showProgress(false)
+ }
+
+ /**
+ * unit test for method UploadMediaPresenter.handleImageResult
+ */
+ @Test
+ fun handleImageResult() {
+ //Positive case test
+ uploadMediaPresenter?.handleImageResult(IMAGE_KEEP)
+ verify(view)?.onImageValidationSuccess()
+
+ //Duplicate file name
+ uploadMediaPresenter?.handleImageResult(FILE_NAME_EXISTS)
+ verify(view)?.showDuplicatePicturePopup()
+
+ //Empty Title test
+ uploadMediaPresenter?.handleImageResult(EMPTY_TITLE)
+ verify(view)?.showMessage(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())
+
+ //Bad Picture test
+ //Empty Title test
+ uploadMediaPresenter?.handleImageResult(-7)
+ verify(view)?.showBadImagePopup(ArgumentMatchers.anyInt())
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt
index ca7a562e6..e27bba4c3 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelTest.kt
@@ -87,24 +87,6 @@ class UploadModelTest {
}
}
- @Test
- fun verifyPreviousNotAvailable() {
- uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
- assertFalse(uploadModel!!.isPreviousAvailable)
- }
-
- @Test
- fun verifyNextAvailable() {
- uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
- assertTrue(uploadModel!!.isNextAvailable)
- }
-
- @Test
- fun isSubmitAvailable() {
- uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
- assertTrue(uploadModel!!.isNextAvailable)
- }
-
@Test
fun getCurrentStep() {
uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
@@ -135,38 +117,6 @@ class UploadModelTest {
}
}
- @Test
- fun isTopCardState() {
- uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
- assertTrue(uploadModel!!.isTopCardState)
- }
-
- @Test
- fun next() {
- uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
- assertTrue(uploadModel!!.currentStep == 1)
- uploadModel!!.next()
- assertTrue(uploadModel!!.currentStep == 2)
- }
-
- @Test
- fun previous() {
- uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
- assertTrue(uploadModel!!.currentStep == 1)
- uploadModel!!.next()
- assertTrue(uploadModel!!.currentStep == 2)
- uploadModel!!.previous()
- assertTrue(uploadModel!!.currentStep == 1)
- }
-
- @Test
- fun isShowingItem() {
- val preProcessImages = uploadModel!!.preProcessImages(getMediaList(), mock(Place::class.java), "external") { _, _ -> }
- preProcessImages.doOnComplete {
- assertTrue(uploadModel!!.isShowingItem)
- }
- }
-
private fun getMediaList(): List {
val element = getElement()
val element2 = getElement()
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt
index 2e17fd1ad..b54ed3b6d 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt
@@ -1,43 +1,82 @@
package fr.free.nrw.commons.upload
+import com.nhaarman.mockito_kotlin.verify
+import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.filepicker.UploadableFile
-import fr.free.nrw.commons.mwapi.MediaWikiApi
-import fr.free.nrw.commons.nearby.Place
+import fr.free.nrw.commons.repository.UploadRepository
import io.reactivex.Observable
import org.junit.Before
import org.junit.Test
-import org.mockito.*
+import org.mockito.ArgumentMatchers
+import org.mockito.InjectMocks
+import org.mockito.Mock
import org.mockito.Mockito.`when`
-import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+import java.util.ArrayList
+
+/**
+ * The clas contains unit test cases for UploadPresenter
+ */
class UploadPresenterTest {
@Mock
- internal var uploadModel: UploadModel? = null
+ internal var repository: UploadRepository? = null
@Mock
- internal var uploadController: UploadController? = null
+ internal var view: UploadContract.View? = null
@Mock
- internal var mediaWikiApi: MediaWikiApi? = null
+ var contribution: Contribution? = null
+
+ @Mock
+ private lateinit var uploadableFile: UploadableFile
@InjectMocks
var uploadPresenter: UploadPresenter? = null
+ private var uploadableFiles: ArrayList = ArrayList()
+
+ /**
+ * initial setup, test environment
+ */
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
- `when`(uploadModel!!.preProcessImages(ArgumentMatchers.anyListOf(UploadableFile::class.java),
- ArgumentMatchers.any(Place::class.java),
- ArgumentMatchers.anyString(),
- ArgumentMatchers.any(SimilarImageInterface::class.java)))
- .thenReturn(Observable.just(mock(UploadModel.UploadItem::class.java)))
+ uploadPresenter?.onAttachView(view)
+ `when`(repository?.buildContributions()).thenReturn(Observable.just(contribution))
+ `when`(view?.isLoggedIn).thenReturn(true)
+ uploadableFiles.add(uploadableFile)
+ `when`(view?.uploadableFiles).thenReturn(uploadableFiles)
+ `when`(uploadableFile?.filePath).thenReturn("data://test")
}
+ /**
+ * unit test case for method UploadPresenter.handleSubmit
+ */
@Test
- fun receiveMultipleItems() {
- val element = Mockito.mock(UploadableFile::class.java)
- val element2 = Mockito.mock(UploadableFile::class.java)
- var uriList: List = mutableListOf(element, element2)
- uploadPresenter!!.receive(uriList, "external", mock(Place::class.java))
+ fun handleSubmitTest() {
+ uploadPresenter?.handleSubmit()
+ verify(view)?.isLoggedIn
+ verify(view)?.showProgress(true)
+ verify(repository)?.buildContributions()
+ val buildContributions = repository?.buildContributions()
+ buildContributions?.test()?.assertNoErrors()?.assertValue {
+ verify(repository)?.prepareService()
+ verify(view)?.showProgress(false)
+ verify(view)?.showMessage(ArgumentMatchers.any(Int::class.java))
+ verify(view)?.finish()
+ true
+ }
+ }
+
+ /**
+ * unit test for UploadMediaPresenter.deletePictureAtIndex
+ */
+ @Test
+ fun deletePictureAtIndexTest() {
+ uploadPresenter?.deletePictureAtIndex(0)
+ verify(repository)?.deletePicture(ArgumentMatchers.anyString())
+ verify(view)?.showMessage(ArgumentMatchers.anyInt())//As there is only one while which we are asking for deletion, upload should be cancelled and this flow should be triggered
+ verify(view)?.finish()
}
}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index ad7e27f69..ac0402861 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.3.2'
+ classpath 'com.android.tools.build:gradle:3.4.1'
classpath 'com.dicedmelon.gradle:jacoco-android:0.1.4'
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
diff --git a/captures/fr.free.nrw.commons_2019.04.15_22.10.li b/captures/fr.free.nrw.commons_2019.04.15_22.10.li
new file mode 100644
index 000000000..9612bf755
Binary files /dev/null and b/captures/fr.free.nrw.commons_2019.04.15_22.10.li differ
diff --git a/gradle.properties b/gradle.properties
index fc38b3a14..742f1f5df 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -25,4 +25,5 @@ DAGGER_VERSION=2.21
systemProp.http.proxyPort=0
systemProp.http.proxyHost=
android.useAndroidX=true
-android.enableJetifier=true
\ No newline at end of file
+android.enableJetifier=true
+android.enableR8=false
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 394c95658..8e0247972 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip