diff --git a/app/build.gradle b/app/build.gradle
index fac1ab9bab..ec84df73e5 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -64,6 +64,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 dffea644c2..b2e434bd4f 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 041fde6b2a..2aa160520f 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 @@
/**
* 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 43c708460e..b7e09d9c2c 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 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 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 0ac5ec8a7a..4aa0627180 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 @@
* 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 CampaignsPresenter(OkHttpJsonApiClient okHttpJsonApiClient) {
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 68f53ca361..9b084da491 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 CategoriesModel(MediaWikiApi mwApi,
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 boolean containsYear(String item) {
|| (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 void updateCategoryCount(CategoryItem item) {
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 Observable searchAll(String term, List imageTitleLi
.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 @@ private Observable directCategories() {
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 void categoryClicked(CategoryItem item) {
}
}
+ /**
+ * 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 List getCategoryStringList() {
}
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 f3ade09d8b..f6c954f43f 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 CategoryItem[] newArray(int i) {
}
};
- 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 2e9ca53279..ec02c73133 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 @@ ContentValues toContentValues(Contribution contribution) {
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/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java
index e63f2b669a..72793f2c83 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.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 @@
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 7f0ee40480..36aba16682 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 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 @@
@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 WikidataEditListener provideWikidataEditListener() {
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 97263d1289..8c5a7bce73 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.NearbyMapFragment;
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 50eb1af562..6ca0a13afe 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 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 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 4521104c75..18349f5259 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
@@ -57,9 +57,6 @@
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;
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 0000000000..3f4a58bd3e
--- /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 0000000000..938b6f30d0
--- /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 0000000000..dbd0f6134d
--- /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 ae18d4adbd..c6f69584ea 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 @@
/**
* 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 @@ void setSelectedLanguageIndex(int selectedLanguageIndex) {
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 7aca519082..a24a791bf7 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 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 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
@Override
public int getItemCount() {
- return descriptions.size() + 1;
+ return descriptions.size();
}
/**
* Gets descriptions
+ *
* @return List of descriptions
*/
- List 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 @@ public class ViewHolder extends RecyclerView.ViewHolder {
AppCompatSpinner spinnerDescriptionLanguages;
@BindView(R.id.description_item_edit_text)
- EditText descItemEditText;
-
- private View view;
+ AppCompatEditText descItemEditText;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
- this.view = itemView;
Timber.i("descItemEditText:" + descItemEditText);
}
- @SuppressLint("ClickableViewAccessibility")
public void init(int position) {
+ Description description = descriptions.get(position);
+ Timber.d("Description is " + description);
+ if (!TextUtils.isEmpty(description.getDescriptionText())) {
+ descItemEditText.setText(description.getDescriptionText());
+ } else {
+ descItemEditText.setText("");
+ }
if (position == 0) {
- Timber.d("Title is " + title);
- if (!title.isEmpty()) {
- descItemEditText.setText(title.toString());
- } else {
- descItemEditText.setText("");
- }
-
- descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null);
-
- descItemEditText.addTextChangedListener(new AbstractTextWatcher(titleText ->{
- 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;
- });
-
- } 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);
+ //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;
- });
- }
-
- descItemEditText.addTextChangedListener(new AbstractTextWatcher(descriptionText->{
- descriptions.get(position - 1).setDescriptionText(descriptionText);
- }));
-
- descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
- if (!hasFocus) {
- ViewUtil.hideKeyboard(v);
- } else {
- uploadView.setTopCardState(false);
}
+ return false;
});
- initLanguageSpinner(position, description);
+ } else {
+ 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 @@ public void init(int position) {
* @param description
*/
private void initLanguageSpinner(int position, Description description) {
- SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context,
+ SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(
+ spinnerDescriptionLanguages.getContext(),
R.layout.row_item_languages_spinner, selectedLanguages);
languagesAdapter.notifyDataSetChanged();
spinnerDescriptionLanguages.setAdapter(languagesAdapter);
- if (description.getSelectedLanguageIndex() == -1) {
- if (position == 1) {
- int defaultLocaleIndex = languagesAdapter.getIndexOfUserDefaultLocale(context);
- spinnerDescriptionLanguages.setSelection(defaultLocaleIndex);
- } else {
- // availableLangIndex gives the index of first non-selected language
- int availableLangIndex = -1;
-
- // loops over the languagesAdapter and finds the index of first non-selected language
- for (int i = 0; i < languagesAdapter.getCount(); i++) {
- if (!selectedLanguages.containsKey(languagesAdapter.getLanguageCode(i))) {
- availableLangIndex = i;
- break;
- }
- }
- if (availableLangIndex >= 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 @@ public void onNothingSelected(AdapterView> adapterView) {
}
});
+
+ if (description.getSelectedLanguageIndex() == -1) {
+ if (position == 0) {
+ int defaultLocaleIndex = languagesAdapter
+ .getIndexOfUserDefaultLocale(spinnerDescriptionLanguages.getContext());
+ spinnerDescriptionLanguages.setSelection(defaultLocaleIndex, true);
+ } else {
+ spinnerDescriptionLanguages.setSelection(0);
+ }
+ } else {
+ spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex());
+ selectedLanguages.put(spinnerDescriptionLanguages, description.getLanguageCode());
+ }
}
/**
* Extracted out the method to get the icon drawable
- * @return
*/
private Drawable getInfoIcon() {
- return context.getResources().getDrawable(R.drawable.mapbox_info_icon_default);
+ return descItemEditText.getContext()
+ .getResources()
+ .getDrawable(R.drawable.mapbox_info_icon_default);
}
}
public interface Callback {
+
void showAlert(int mediaDetailDescription, int descriptionInfo);
}
+
+ /**
+ * 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);
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java
index 1d4497586e..e9922332c4 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java
@@ -6,6 +6,7 @@
import android.net.Uri;
import androidx.annotation.NonNull;
+import fr.free.nrw.commons.upload.SimilarImageDialogFragment.Callback;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
@@ -37,7 +38,7 @@
* Processing of the image filePath that is about to be uploaded via ShareActivity is done here
*/
@Singleton
-public class FileProcessor implements SimilarImageDialogFragment.onResponse {
+public class FileProcessor implements Callback {
@Inject
CacheController cacheController;
@@ -58,7 +59,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject
- FileProcessor() {
+ public FileProcessor() {
}
public void cleanup() {
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java
index e45e7ec478..ea908f6846 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java
@@ -13,12 +13,12 @@
* Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation
* is uploaded, extract latitude and longitude from EXIF data of image.
*/
-class GPSExtractor {
+public class GPSExtractor {
static final GPSExtractor DUMMY= new GPSExtractor();
private double decLatitude;
private double decLongitude;
- boolean imageCoordsExists;
+ public boolean imageCoordsExists;
private String latitude;
private String longitude;
private String latitudeRef;
@@ -96,11 +96,11 @@ String getCoords() {
}
}
- double getDecLatitude() {
+ public double getDecLatitude() {
return decLatitude;
}
- double getDecLongitude() {
+ public double getDecLongitude() {
return decLongitude;
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java
index 20e088cbb6..eb0b2e7e5f 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java
@@ -37,17 +37,21 @@ public class SimilarImageDialogFragment extends DialogFragment {
Button positiveButton;
@BindView(R.id.negative_button)
Button negativeButton;
- onResponse mOnResponse;//Implemented interface from shareActivity
+ Callback callback;//Implemented interface from shareActivity
Boolean gotResponse = false;
public SimilarImageDialogFragment() {
}
- public interface onResponse{
+ public interface Callback {
void onPositiveResponse();
void onNegativeResponse();
}
+ public void setCallback(Callback callback) {
+ this.callback = callback;
+ }
+
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false);
@@ -77,7 +81,6 @@ R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mOnResponse = (onResponse) getActivity();//Interface Implementation
}
@Override
@@ -91,21 +94,21 @@ public Dialog onCreateDialog(Bundle savedInstanceState) {
public void onDismiss(DialogInterface dialog) {
// I user dismisses dialog by pressing outside the dialog.
if (!gotResponse) {
- mOnResponse.onNegativeResponse();
+ callback.onNegativeResponse();
}
super.onDismiss(dialog);
}
@OnClick(R.id.negative_button)
public void onNegativeButtonClicked() {
- mOnResponse.onNegativeResponse();
+ callback.onNegativeResponse();
gotResponse = true;
dismiss();
}
@OnClick(R.id.postive_button)
public void onPositiveButtonClicked() {
- mOnResponse.onPositiveResponse();
+ callback.onPositiveResponse();
gotResponse = true;
dismiss();
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java
index 10d3e2484b..346f6a4ec1 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java
@@ -7,7 +7,6 @@
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
-import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
@@ -22,6 +21,10 @@
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.BiMap;
import fr.free.nrw.commons.utils.LangCodeUtils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
public class SpinnerLanguagesAdapter extends ArrayAdapter {
@@ -83,27 +86,32 @@ public int getCount() {
@Override
public View getDropDownView(int position, @Nullable View convertView,
@NonNull ViewGroup parent) {
- View view = layoutInflater.inflate(resource, parent, false);
- ViewHolder holder = new ViewHolder(view);
+ if (convertView == null) {
+ convertView = layoutInflater.inflate(resource, parent, false);
+ }
+ ViewHolder holder = new ViewHolder(convertView);
holder.init(position, true);
- return view;
+ return convertView;
}
@Override
public @NonNull
View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
- View view = layoutInflater.inflate(resource, parent, false);
- ViewHolder holder = new ViewHolder(view);
+ ViewHolder holder;
+ if (convertView == null) {
+ convertView = layoutInflater.inflate(resource, parent, false);
+ holder = new ViewHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
holder.init(position, false);
- return view;
+ return convertView;
}
public class ViewHolder {
- @BindView(R.id.ll_container_description_language)
- LinearLayout llContainerDescriptionLanguage;
-
@BindView(R.id.tv_language)
TextView tvLanguage;
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java
index 8963d0e257..371e5ee106 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java
@@ -1,5 +1,7 @@
package fr.free.nrw.commons.upload;
+import fr.free.nrw.commons.filepicker.UploadableFile;
+
public interface ThumbnailClickedListener {
- void thumbnailClicked(UploadModel.UploadItem content);
+ void thumbnailClicked(UploadableFile content);
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.java
new file mode 100644
index 0000000000..b32f90b0be
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.java
@@ -0,0 +1,112 @@
+package fr.free.nrw.commons.upload;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import com.facebook.drawee.view.SimpleDraweeView;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.filepicker.UploadableFile;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The adapter class for image thumbnails to be shown while uploading.
+ */
+class ThumbnailsAdapter 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 bc2d55640d..380b2c1deb 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 void setSet(boolean set) {
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 347d6e71c4..8881151e18 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,143 +1,104 @@
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;
-
- // 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;
-
- // 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.right_card_map_button) View rightCardMapButton;
-
- // 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;
-
- // 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;
-
- private DescriptionsAdapter descriptionsAdapter;
- private RVRendererAdapter categoriesAdapter;
+ @BindView(R.id.cv_container_top_card)
+ CardView cvContainerTopCard;
+
+ @BindView(R.id.ll_container_top_card)
+ LinearLayout llContainerTopCard;
+
+ @BindView(R.id.rl_container_title)
+ RelativeLayout rlContainerTitle;
+
+ @BindView(R.id.tv_top_card_title)
+ TextView tvTopCardTitle;
+
+ @BindView(R.id.ib_toggle_top_card)
+ ImageButton ibToggleTopCard;
+
+ @BindView(R.id.rv_thumbnails)
+ RecyclerView rvThumbnails;
+
+ @BindView(R.id.vp_upload)
+ ViewPager vpUpload;
+
+ 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
@@ -145,18 +106,10 @@ 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();
+ ButterKnife.bind(this);
+ compositeDisposable = new CompositeDisposable();
+ init();
PermissionUtils.checkPermissionsAndPerformAction(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
@@ -165,282 +118,149 @@ protected void onCreate(Bundle savedInstanceState) {
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;
- }
-
- @Override
- protected void onDestroy() {
- presenter.cleanup();
- super.onDestroy();
+ private void init() {
+ initProgressDialog();
+ initViewPager();
+ initThumbnailsRecyclerView();
+ //And init other things you need to
}
- @Override
- protected void onResume() {
- super.onResume();
- checkIfLoggedIn();
-
- 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),
- R.string.storage_permission_title,
- R.string.write_storage_permission_rationale_for_image_share);
+ private void initProgressDialog() {
+ progressDialog = new ProgressDialog(this);
+ progressDialog.setMessage(getString(R.string.please_wait));
}
- @Override
- protected void onPause() {
- presenter.removeView();
- super.onPause();
- }
+ private void initThumbnailsRecyclerView() {
+ rvThumbnails.setLayoutManager(new LinearLayoutManager(this,
+ LinearLayoutManager.HORIZONTAL, false));
+ thumbnailsAdapter=new ThumbnailsAdapter(() -> currentSelectedPosition);
+ rvThumbnails.setAdapter(thumbnailsAdapter);
- @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);
- }
+ 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 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);
+ @Override
+ public void onPageSelected(int position) {
+ currentSelectedPosition=position;
+ if (position >= uploadableFiles.size()) {
+ cvContainerTopCard.setVisibility(View.GONE);
+ } else {
+ thumbnailsAdapter.notifyDataSetChanged();
+ cvContainerTopCard.setVisibility(View.VISIBLE);
+ }
- 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;
- }
+ @Override
+ public void onPageScrollStateChanged(int state) {
- 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));
+ public boolean isLoggedIn() {
+ return sessionManager.isUserLoggedIn();
}
@Override
- public void updateTopCardContent() {
- RecyclerView.Adapter adapter = topCardThumbnails.getAdapter();
- if (adapter != null) {
- adapter.notifyDataSetChanged();
+ protected void onResume() {
+ super.onResume();
+ presenter.onAttachView(this);
+ if (!isLoggedIn()) {
+ askUserToLogIn();
}
+ checkStoragePermissions();
}
- @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);
+ private void checkStoragePermissions() {
+ PermissionUtils.checkPermissionsAndPerformAction(this,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ () -> {
+ //TODO handle this
+ },
+ R.string.storage_permission_title,
+ R.string.write_storage_permission_rationale_for_image_share);
}
- @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);
+ public void showProgress(boolean shouldShow) {
+ if (shouldShow) {
+ if (!progressDialog.isShowing()) {
+ progressDialog.show();
+ }
+ } else {
+ if (progressDialog != null && !isFinishing()) {
+ progressDialog.dismiss();
+ }
+ }
}
@Override
- public void setBottomCardState(boolean state) {
- updateCardState(state, bottomCardExpandButton, rvDescriptions, previous, next, prevTitleDecs, bottomCardAddDescription);
+ public int getIndexInViewFlipper(UploadBaseFragment fragment) {
+ return fragments.indexOf(fragment);
}
-
@Override
- public void setBackground(Uri mediaUri) {
- background.setImageURI(mediaUri);
+ public int getTotalNumberOfSteps() {
+ return fragments.size();
}
-
@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 showMessage(int messageResourceId) {
+ ViewUtil.showLongToast(this, messageResourceId);
}
@Override
- public void showBadPicturePopup(String errorMessage) {
- DialogUtil.showAlertDialog(this,
- getString(R.string.warning),
- errorMessage,
- () -> presenter.deletePicture(),
- () -> presenter.keepPicture());
+ public List getUploadableFiles() {
+ return uploadableFiles;
}
@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 void showHideTopCard(boolean shouldShow) {
+ llContainerTopCard.setVisibility(shouldShow?View.VISIBLE:View.GONE);
}
@Override
- public void showProgressDialog() {
- if (progressDialog == null) {
- progressDialog = new ProgressDialog(this);
- }
- progressDialog.setMessage(getString(R.string.please_wait));
- progressDialog.show();
+ 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 hideProgressDialog() {
- if (progressDialog != null && !isFinishing()) {
- progressDialog.dismiss();
- }
+ public void updateTopCardTitle() {
+ tvTopCardTitle.setText(getResources()
+ .getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size()));
}
@Override
- public void launchMapActivity(LatLng decCoords) {
- Utils.handleGeoCoordinates(this, decCoords);
+ 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
- public void showErrorMessage(int resourceId) {
- ViewUtil.showShortToast(this, resourceId);
- }
-
- @Override
- public void initDefaultCategories() {
- updateCategoryList("");
- }
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
@@ -450,202 +270,87 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
}
}
- 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);
+ private void receiveSharedItems() {
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ receiveExternalSharedItems();
+ } else if (ACTION_INTERNAL_UPLOADS.equals(action)) {
+ receiveInternalSharedItems();
}
- 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_";
+ if (uploadableFiles == null || uploadableFiles.isEmpty()) {
+ handleNullMedia();
} 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;
+ //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);
}
- setTitleAndDescriptions();
- if (multipleUpload) {
- savePrevTitleDesc("prev_image_");
+ 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);
}
- 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();
- });
- }
+ uploadCategoriesFragment = new UploadCategoriesFragment();
+ uploadCategoriesFragment.setCallback(this);
- private void setTitleAndDescriptions() {
- List descriptions = descriptionsAdapter.getDescriptions();
- Timber.d("Descriptions size is %d are %s", descriptions.size(), descriptions);
- }
+ mediaLicenseFragment = new MediaLicenseFragment();
+ mediaLicenseFragment.setCallback(this);
- 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");
- }
- }
- ));
- }
+ fragments.add(uploadCategoriesFragment);
+ fragments.add(mediaLicenseFragment);
- private void receiveSharedItems() {
- Intent intent = getIntent();
- String action = intent.getAction();
- if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
- receiveExternalSharedItems();
- } else if (ACTION_INTERNAL_UPLOADS.equals(action)) {
- receiveInternalSharedItems();
+ 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 @@ private void receiveInternalSharedItems() {
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 @@ private void handleNullMedia() {
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 @@ private void showInfoAlert(int titleStringID, int messageStringId, String... for
}
@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 onNextButtonClicked(int index) {
+ if (index < fragments.size()-1) {
+ vpUpload.setCurrentItem(index + 1, false);
+ } else {
+ presenter.handleSubmit();
+ }
}
- public void savePrevTitleDesc(String name){
+ @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();
+ }
- 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());
+ @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 0000000000..afd1a694b5
--- /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 0000000000..f90496da01
--- /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 34603f0b8a..1a602a0017 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 void onServiceDisconnected(ComponentName componentName) {
/**
* 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 @@ void prepareService() {
/**
* Disconnects the upload service.
*/
- void cleanup() {
+ public void cleanup() {
if (isUploadServiceConnected) {
context.unbindService(uploadServiceConnection);
}
@@ -96,7 +96,7 @@ void cleanup() {
*
* @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 c94f9f4967..65f12354bf 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 @@
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.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));
+ }
+
+
+ /**
+ * 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));
}
- Single getImageQuality(UploadItem uploadItem, boolean checkTitle) {
+ 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 @@ private UploadItem getUploadItem(UploadableFile uploadableFile,
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 List getUploads() {
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 @@ Observable buildContributions(List categoryStringList) {
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 @@ Observable buildContributions(List categoryStringList) {
});
}
- 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 List getItems() {
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 @@ static class UploadItem {
@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 String getFileExt() {
}
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 0000000000..9e4f572a79
--- /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 5e0dd32311..a08a547a9b 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 {
-
- private static final UploadView DUMMY =
- (UploadView) CustomProxy.newInstance(UploadView.class.getClassLoader(),
- new Class[] { UploadView.class });
-
- private UploadView view = DUMMY;
+public class UploadPresenter implements UploadContract.UserActionListener {
- private static final SimilarImageInterface SIMILAR_IMAGE =
- (SimilarImageInterface) CustomProxy.newInstance(
- SimilarImageInterface.class.getClassLoader(),
- new Class[] { SimilarImageInterface.class });
- private SimilarImageInterface similarImageInterface = SIMILAR_IMAGE;
+ 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;
- @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;
- }
-
- /**
- * 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);
+ UploadPresenter(UploadRepository uploadRepository) {
+ this.repository = uploadRepository;
+ compositeDisposable = new CompositeDisposable();
}
- /**
- * 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));
- }
-
- /**
- * 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));
+ .subscribe(new Observer() {
+ @Override
+ public void onSubscribe(Disposable d) {
+ view.showProgress(false);
+ view.showMessage(R.string.uploading_started);
+ compositeDisposable.add(d);
+ }
+
+ @Override
+ public void onNext(Contribution contribution) {
+ repository.startUpload(contribution);
+ }
+
+ @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());
+ }
+
+ @Override
+ public void onComplete() {
+ repository.cleanup();
+ view.finish();
+ compositeDisposable.clear();
+ }
+ });
+ } else {
+ view.askUserToLogIn();
}
}
- void keepPicture() {
- uploadModel.keepPicture();
- }
-
- void deletePicture() {
- if (uploadModel.getCount() == 1)
- view.finish();
- else {
- uploadModel.deletePicture();
- updateCards();
- updateContent();
- uploadModel.subscribeBadPicture(this::handleBadImage, false);
- view.dismissKeyboard();
+ @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);
}
- }
- //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);
+ //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 (uploadModel.isBottomCardState()) {
- uploadModel.setBottomCardState(false);
- view.setBottomCardState(false);
+ if (uploadableFiles.size() < 2) {
+ view.showHideTopCard(false);
}
- }
- //endregion
- //region View / Lifecycle management
- public void init() {
- uploadController.prepareService();
- }
-
- void cleanup() {
- compositeDisposable.clear();
- uploadModel.cleanup();
- uploadController.cleanup();
- }
+ //In case lets update the number of uploadable media
+ view.updateTopCardTitle();
- void removeView() {
- this.view = DUMMY;
}
- void addView(UploadView view) {
+ @Override
+ public void onAttachView(UploadContract.View view) {
this.view = view;
-
- updateCards();
- updateLicenses();
- updateContent();
+ repository.prepareService();
}
-
- /**
- * 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();
- } else {
- currentPage = UploadView.LICENSE;
- view.setTopCardVisibility(false);
- view.setRightCardVisibility(false);
- }
- view.setBottomCardVisibility(currentPage, uploadCount);
- }
-
- //endregion
-
- /**
- * @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 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 afcc42aed9..0000000000
--- 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 3ea4dfa62a..e69de29bb2 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 9fb50c7ca3..ec1854ffcc 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 0000000000..6ff51632a8
--- /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 0000000000..a0a7762462
--- /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 0000000000..48485c17ad
--- /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 0000000000..68e6affb47
--- /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 0000000000..8836c9bdfc
--- /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 0000000000..881f21369c
--- /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 0000000000..0b589fc77b
--- /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 0000000000..9447000ab6
--- /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 0000000000..3cccfe89da
--- /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 53d129fb3a..e9765551ce 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 @@ private static void showAlertDialog(Activity activity,
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/res/drawable/drawable_thumbnail_image.xml b/app/src/main/res/drawable/drawable_thumbnail_image.xml
new file mode 100644
index 0000000000..b406e8938a
--- /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 0000000000..8ead4b377d
--- /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 0000000000..ac6ec93355
--- /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 3987a53d34..7822ce5930 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 3afe769dff..0000000000
--- 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 61f35cc33a..0000000000
--- 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 0f5e94d31f..0000000000
--- 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 008f37c832..0000000000
--- 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 0000000000..4b46ebb6f0
--- /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 0000000000..84003afa6e
--- /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 6a91afb649..9b8e8ee743 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 c2f520ca74..32136b29dc 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-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 875ab48821..5b48f71ae1 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/colors.xml b/app/src/main/res/values/colors.xml
index a580effd82..1c6f36aee7 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 666ad5a641..0c6ba28f06 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,5 +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 0000000000..eae8defc58
--- /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 0000000000..6d64f63102
--- /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 0000000000..1887857578
--- /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 ca7a562e61..e27bba4c38 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 2e17fd1adb..b54ed3b6d5 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 view: UploadContract.View? = null
@Mock
- internal var uploadController: UploadController? = null
+ var contribution: Contribution? = null
+
@Mock
- internal var mediaWikiApi: MediaWikiApi? = null
+ 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 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 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 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/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 0000000000..9612bf7556
Binary files /dev/null and b/captures/fr.free.nrw.commons_2019.04.15_22.10.li differ