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 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -