From b3348a618f4d8ba25f3dd1fbfb62f938a6e02eec Mon Sep 17 00:00:00 2001 From: maskara Date: Mon, 5 Nov 2018 00:57:14 +0530 Subject: [PATCH] Changes to fix pending issues with upload flow --- .../nrw/commons/upload/FileUtilsTest.java | 30 -- .../nrw/commons/category/CategoriesModel.java | 28 +- .../category/CategorizationFragment.java | 282 ------------------ .../nrw/commons/di/FragmentBuilderModule.java | 4 - .../commons/upload/DescriptionsAdapter.java | 17 +- .../nrw/commons/upload/UploadActivity.java | 111 ++++--- .../free/nrw/commons/upload/UploadModel.java | 7 +- .../nrw/commons/upload/UploadPresenter.java | 125 +++++++- .../free/nrw/commons/upload/UploadView.java | 6 + .../fr/free/nrw/commons/utils/ImageUtils.java | 8 +- .../free/nrw/commons/utils/StringUtils.java | 4 + .../fr/free/nrw/commons/utils/ViewUtil.java | 25 ++ .../layout/activity_upload_bottom_card.xml | 14 +- .../res/layout/activity_upload_categories.xml | 18 +- .../res/layout/activity_upload_license.xml | 19 +- .../res/layout/fragment_categorization.xml | 71 ----- app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 13 +- 18 files changed, 298 insertions(+), 485 deletions(-) delete mode 100644 app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java delete mode 100644 app/src/main/res/layout/fragment_categorization.xml diff --git a/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java b/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java deleted file mode 100644 index 636d30a1bfc..00000000000 --- a/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.net.Uri; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import fr.free.nrw.commons.BuildConfig; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -@RunWith(AndroidJUnit4.class) -public class FileUtilsTest { - @Test - public void isSelfOwned() throws Exception { - Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".provider/document/1"); - boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri); - assertThat(selfOwned, is(true)); - } - - @Test - public void isNotSelfOwned() throws Exception { - Uri uri = Uri.parse("content://com.android.providers.media.documents/document/1"); - boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri); - assertThat(selfOwned, is(false)); - } -} 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 d23c93c2a2c..8375f4972c4 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 @@ -32,7 +32,8 @@ public class CategoriesModel implements CategoryClickedListener { @Inject GpsCategoryModel gpsCategoryModel; @Inject - public CategoriesModel(MediaWikiApi mwApi, CategoryDao categoryDao, + public CategoriesModel(MediaWikiApi mwApi, + CategoryDao categoryDao, @Named("default_preferences") SharedPreferences prefs, @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs) { this.mwApi = mwApi; @@ -97,11 +98,11 @@ boolean cacheContainsKey(String term) { //endregion //region Category searching - public Observable searchAll(String term) { + public Observable searchAll(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()) + .concatWith(titleCategories(imageTitleList)) .concatWith(recentCategories()); } @@ -117,11 +118,11 @@ public Observable searchAll(String term) { .map(name -> new CategoryItem(name, false)); } - public Observable searchCategories(String term) { + 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()) + .concatWith(titleCategories(imageTitleList)) .concatWith(recentCategories()); } @@ -134,18 +135,18 @@ private ArrayList getCachedCategories(String term) { return categoriesCache.get(term); } - public Observable defaultCategories() { + public Observable defaultCategories(List titleList) { Observable directCat = directCategories(); if (hasDirectCategories()) { Timber.d("Image has direct Cat"); return directCat .concatWith(gpsCategories()) - .concatWith(titleCategories()) + .concatWith(titleCategories(titleList)) .concatWith(recentCategories()); } else { Timber.d("Image has no direct Cat"); return gpsCategories() - .concatWith(titleCategories()) + .concatWith(titleCategories(titleList)) .concatWith(recentCategories()); } } @@ -171,12 +172,13 @@ Observable gpsCategories() { .map(name -> new CategoryItem(name, false)); } - private Observable titleCategories() { - //Retrieve the title that was saved when user tapped submit icon - String title = prefs.getString("Title", ""); + private Observable titleCategories(List titleList) { + return Observable.fromIterable(titleList) + .concatMap(this::getTitleCategories); + } - return mwApi - .searchTitles(title, SEARCH_CATS_LIMIT) + private Observable getTitleCategories(String title) { + return mwApi.searchTitles(title, SEARCH_CATS_LIMIT) .map(name -> new CategoryItem(name, false)); } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java deleted file mode 100644 index 26b536456c9..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ /dev/null @@ -1,282 +0,0 @@ -package fr.free.nrw.commons.category; - - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.jakewharton.rxbinding2.view.RxView; -import com.jakewharton.rxbinding2.widget.RxTextView; -import com.pedrogomez.renderers.RVRendererAdapter; - -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 java.util.concurrent.TimeUnit; - -import javax.inject.Inject; -import javax.inject.Named; - -import butterknife.BindView; -import butterknife.ButterKnife; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; - -import static android.view.KeyEvent.ACTION_UP; -import static android.view.KeyEvent.KEYCODE_BACK; - -/** - * Displays the category suggestion and selection screen. Category search is initiated here. - */ -public class CategorizationFragment extends CommonsDaggerSupportFragment { - - public static final int SEARCH_CATS_LIMIT = 25; - - @BindView(R.id.categoriesListBox) - RecyclerView categoriesList; - @BindView(R.id.categoriesSearchBox) - EditText categoriesFilter; - @BindView(R.id.categoriesSearchInProgress) - ProgressBar categoriesSearchInProgress; - @BindView(R.id.categoriesNotFound) - TextView categoriesNotFoundView; - @BindView(R.id.categoriesExplanation) - TextView categoriesSkip; - - @Inject CategoriesModel categoriesModel; - @Inject MediaWikiApi mwApi; - @Inject @Named("default_preferences") SharedPreferences prefs; - @Inject @Named("prefs") SharedPreferences prefsPrefs; - @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; - @Inject CategoryDao categoryDao; - - private CategoryRendererAdapter categoriesAdapter; - private OnCategoriesSaveHandler onCategoriesSaveHandler; - private TitleTextWatcher textWatcher = new TitleTextWatcher(); - - @SuppressLint("CheckResult") - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_categorization, container, false); - ButterKnife.bind(this, rootView); - - categoriesList.setLayoutManager(new LinearLayoutManager(getContext())); - - ArrayList items = new ArrayList<>(); - if (savedInstanceState != null) { - items.addAll(savedInstanceState.getParcelableArrayList("currentCategories")); - //noinspection unchecked - categoriesModel.cacheAll((HashMap>) savedInstanceState - .getSerializable("categoriesCache")); - } - - CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> { - if (item.isSelected()) { - categoriesModel.selectCategory(item); - categoriesModel.updateCategoryCount(item); - } else { - categoriesModel.unselectCategory(item); - } - }); - categoriesAdapter = adapterFactory.create(items); - categoriesList.setAdapter(categoriesAdapter); - - categoriesFilter.addTextChangedListener(textWatcher); - - categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> { - if (!hasFocus) { - ViewUtil.hideKeyboard(v); - } - }); - - RxTextView.textChanges(categoriesFilter) - .takeUntil(RxView.detaches(categoriesFilter)) - .debounce(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(filter -> updateCategoryList(filter.toString())); - return rootView; - } - - @Override - public void onDestroyView() { - categoriesFilter.removeTextChangedListener(textWatcher); - super.onDestroyView(); - } - - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - menu.clear(); - inflater.inflate(R.menu.fragment_categorization, menu); - } - - @Override - public void onResume() { - super.onResume(); - - View rootView = getView(); - if (rootView != null) { - rootView.setFocusableInTouchMode(true); - rootView.requestFocus(); - rootView.setOnKeyListener((v, keyCode, event) -> { - if (event.getAction() == ACTION_UP && keyCode == KEYCODE_BACK) { - showBackButtonDialog(); - return true; - } - return false; - }); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putParcelableArrayList("currentCategories", categoriesAdapter.allItems()); - outState.putSerializable("categoriesCache", categoriesModel.getCategoriesCache()); - } - - @Override - public boolean onOptionsItemSelected(MenuItem menuItem) { - switch (menuItem.getItemId()) { - case R.id.menu_save_categories: - if (categoriesModel.selectedCategoriesCount() > 0) { - //Some categories selected, proceed to submission - onCategoriesSaveHandler.onCategoriesSave(categoriesModel.getCategoryStringList()); - } else { - //No categories selected, prompt the user to select some - showConfirmationDialog(); - } - return true; - default: - return super.onOptionsItemSelected(menuItem); - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(true); - onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity(); - getActivity().setTitle(R.string.categories_activity_title); - } - - @SuppressLint({"StringFormatInvalid", "CheckResult"}) - private void updateCategoryList(String filter) { - Observable.fromIterable(categoriesModel.getSelectedCategories()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe(disposable -> { - categoriesSearchInProgress.setVisibility(View.VISIBLE); - categoriesNotFoundView.setVisibility(View.GONE); - categoriesSkip.setVisibility(View.GONE); - categoriesAdapter.clear(); - }) - .observeOn(Schedulers.io()) - .concatWith( - categoriesModel.searchAll(filter) - .mergeWith(categoriesModel.searchCategories(filter)) - .concatWith(TextUtils.isEmpty(filter) - ? categoriesModel.defaultCategories() : 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()) { - // There are no suggestions - if (TextUtils.isEmpty(filter)) { - // Allow to send image with no categories - categoriesSkip.setVisibility(View.VISIBLE); - } else { - // Inform the user that the searched term matches no category - categoriesNotFoundView.setText(getString(R.string.categories_not_found, filter)); - categoriesNotFoundView.setVisibility(View.VISIBLE); - } - } - } - ); - } - - /** - * Show dialog asking for confirmation to leave without saving categories. - */ - public void showBackButtonDialog() { - new AlertDialog.Builder(getActivity()) - .setMessage("Are you sure you want to go back? The image will not " - + "have any categories saved.") - .setTitle("Warning") - .setPositiveButton(android.R.string.no, (dialog, id) -> { - //No need to do anything, user remains on categorization screen - }) - .setNegativeButton(android.R.string.yes, (dialog, id) -> getActivity().finish()) - .create() - .show(); - } - - private void showConfirmationDialog() { - new AlertDialog.Builder(getActivity()) - .setMessage("Images without categories are rarely usable. " - + "Are you sure you want to submit without selecting " - + "categories?") - .setTitle("No Categories Selected") - .setPositiveButton(android.R.string.no, (dialog, id) -> { - //Exit menuItem so user can select their categories - }) - .setNegativeButton(android.R.string.yes, (dialog, id) -> { - //Proceed to submission - onCategoriesSaveHandler.onCategoriesSave(categoriesModel.getCategoryStringList()); - }) - .create() - .show(); - } - - private class TitleTextWatcher implements TextWatcher { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void afterTextChanged(Editable editable) { - if (getActivity() != null) { - getActivity().invalidateOptionsMenu(); - } - } - } -} 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 517ae064c99..3fe0a9a13ab 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 @@ -4,7 +4,6 @@ import dagger.android.ContributesAndroidInjector; import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; -import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.CategoryImagesListFragment; import fr.free.nrw.commons.category.SubCategoryListFragment; import fr.free.nrw.commons.contributions.ContributionsListFragment; @@ -22,9 +21,6 @@ @SuppressWarnings({"WeakerAccess", "unused"}) public abstract class FragmentBuilderModule { - @ContributesAndroidInjector - abstract CategorizationFragment bindCategorizationFragment(); - @ContributesAndroidInjector abstract ContributionsListFragment bindContributionsListFragment(); 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 d103910327d..b43fe651a0b 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 @@ -27,7 +27,6 @@ import fr.free.nrw.commons.utils.AbstractTextWatcher; import fr.free.nrw.commons.utils.BiMap; import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; import io.reactivex.subjects.BehaviorSubject; import io.reactivex.subjects.Subject; import timber.log.Timber; @@ -43,23 +42,21 @@ class DescriptionsAdapter extends RecyclerView.Adapter titleChangedSubject; private BiMap selectedLanguages; + private UploadView uploadView; - public DescriptionsAdapter() { + public DescriptionsAdapter(UploadView uploadView) { title = new Title(); descriptions = new ArrayList<>(); descriptions.add(new Description()); titleChangedSubject = BehaviorSubject.create(); selectedLanguages = new BiMap<>(); + this.uploadView = uploadView; } public void setCallback(Callback callback) { this.callback = callback; } - public Observable getTitleChangeObserver() { - return titleChangedSubject; - } - public void setItems(Title title, List descriptions) { this.descriptions = descriptions; this.title = title; @@ -154,6 +151,8 @@ public void init(int position) { descItemEditText.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { ViewUtil.hideKeyboard(v); + } else { + uploadView.setTopCardState(false); } }); @@ -177,6 +176,8 @@ public void init(int position) { descItemEditText.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { ViewUtil.hideKeyboard(v); + } else { + uploadView.setTopCardState(false); } }); @@ -208,10 +209,6 @@ public void onItemSelected(AdapterView adapterView, View view, int position, selectedLanguages.remove(adapterView); selectedLanguages.put(adapterView, languageCode); ((SpinnerLanguagesAdapter) adapterView.getAdapter()).selectedLangCode = languageCode; -// if(prevSpinnerItem!=null) -// prevSpinnerItem.setText(prevSpinnerItem.getText().subSequence(0,2)); -// prevSpinnerItem=(TextView)adapterView.getItemAtPosition(position); -// prevSpinnerItem.setText(languageCode); } @Override 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 37e15236577..44c995cf0c1 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 @@ -3,7 +3,6 @@ import android.Manifest; import android.animation.LayoutTransition; import android.annotation.SuppressLint; -import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; @@ -38,7 +37,6 @@ import com.pedrogomez.renderers.RVRendererAdapter; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.concurrent.TimeUnit; @@ -88,6 +86,7 @@ public class UploadActivity extends AuthenticatedActivity implements UploadView @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; @@ -131,17 +130,15 @@ protected void onCreate(Bundle savedInstanceState) { ButterKnife.bind(this); compositeDisposable = new CompositeDisposable(); - configureCategories(savedInstanceState); - configureLicenses(); configureLayout(); configureTopCard(); configureBottomCard(); initRecyclerView(); configureRightCard(); configureNavigationButtons(); + configureCategories(); + configureLicenses(); - //storagePermissionReady = BehaviorSubject.createDefault(false); - //storagePermissionReady.subscribe(b -> Timber.i("storagePermissionReady:" + b)); presenter.initFromSavedState(savedInstanceState); dexterPermissionObtainer = new DexterPermissionObtainer(this, @@ -190,8 +187,6 @@ protected void onResume() { @Override protected void onPause() { presenter.removeView(); -// imageTitle.removeTextChangedListener(titleWatcher); -// imageDescription.removeTextChangedListener(descriptionWatcher); compositeDisposable.dispose(); compositeDisposable = new CompositeDisposable(); super.onPause(); @@ -218,13 +213,13 @@ public void updateRightCardContent(boolean gpsPresent) { @Override public void updateBottomCardContent(int currentStep, int stepCount, UploadModel.UploadItem uploadItem) { 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 (!uploadItem.isDummy()) { - descriptionsAdapter.setItems(uploadItem.title, uploadItem.descriptions); - rvDescriptions.setAdapter(descriptionsAdapter); - } + descriptionsAdapter.setItems(uploadItem.title, uploadItem.descriptions); + rvDescriptions.setAdapter(descriptionsAdapter); } @Override @@ -244,6 +239,7 @@ public void updateLicenses(List licenses, String selectedLicense) { licenseSpinner.setSelection(position); } + @SuppressLint("StringFormatInvalid") @Override public void updateLicenseSummary(String selectedLicense) { String licenseHyperLink = "" + @@ -338,26 +334,30 @@ public void dismissKeyboard() { @Override public void showBadPicturePopup(@ImageUtils.Result int result) { - int errorMessage; + String errorTitle = getString(R.string.warning); + String errorMessage; if (result == ImageUtils.IMAGE_DARK) - errorMessage = R.string.upload_image_problem_dark; + errorMessage = getString(R.string.upload_image_problem_dark); else if (result == ImageUtils.IMAGE_BLURRY) - errorMessage = R.string.upload_image_problem_blurry; + errorMessage = getString(R.string.upload_image_problem_blurry); else if (result == ImageUtils.IMAGE_DUPLICATE) - errorMessage = R.string.upload_image_problem_duplicate; + errorMessage = getString(R.string.upload_image_problem_duplicate); else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY)) - errorMessage = R.string.upload_image_problem_dark_blurry; + errorMessage = getString(R.string.upload_image_problem_dark_blurry); else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_DUPLICATE)) - errorMessage = R.string.upload_image_problem_dark_duplicate; + errorMessage = getString(R.string.upload_image_problem_dark_duplicate); else if (result == (ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE)) - errorMessage = R.string.upload_image_problem_blurry_duplicate; + errorMessage = getString(R.string.upload_image_problem_blurry_duplicate); else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE)) - errorMessage = R.string.upload_image_problem_dark_blurry_duplicate; + errorMessage = getString(R.string.upload_image_problem_dark_blurry_duplicate); + else if (result == ImageUtils.FILE_NAME_EXISTS) + errorMessage = String.format(getString(R.string.upload_title_duplicate), presenter.getCurrentImageFileName()); else return; + AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(this); errorDialogBuilder.setMessage(errorMessage); - errorDialogBuilder.setTitle(R.string.warning); + errorDialogBuilder.setTitle(errorTitle); //user does not wish to upload the picture, delete it errorDialogBuilder.setPositiveButton(R.string.no, (dialogInterface, i) -> { presenter.deletePicture(); @@ -367,6 +367,29 @@ else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY|ImageUtils.IMA errorDialogBuilder.setNegativeButton(R.string.yes, (DialogInterface dialogInterface, int i) -> { presenter.keepPicture(); dialogInterface.dismiss(); + if(result == ImageUtils.FILE_NAME_EXISTS) { + presenter.handleNext(categoriesModel, false); + } + }); + + AlertDialog errorDialog = errorDialogBuilder.create(); + if (!isFinishing()) { + errorDialog.show(); + } + } + + public void showNoCategorySelectedWarning() { + AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(this); + errorDialogBuilder.setMessage(R.string.no_categories_selected_warning_desc); + errorDialogBuilder.setTitle(R.string.no_categories_selected); + //user does not wish to upload the picture, delete it + errorDialogBuilder.setPositiveButton(R.string.no_go_back, (dialogInterface, i) -> { + dialogInterface.dismiss(); + }); + //user wishes to go ahead with the upload of this picture, just dismiss this dialog + errorDialogBuilder.setNegativeButton(R.string.yes_submit, (DialogInterface dialogInterface, int i) -> { + presenter.handleNext(categoriesModel, true); + dialogInterface.dismiss(); }); AlertDialog errorDialog = errorDialogBuilder.create(); @@ -380,8 +403,14 @@ public void launchMapActivity(String decCoords) { Utils.handleGeoCoordinates(this, decCoords); } - public void showDuplicateTitlePopup(String title) { - showInfoAlert(R.string.warning, R.string.upload_title_duplicate, title); + @Override + public void showErrorMessage(int resourceId) { + ViewUtil.showShortToast(this, resourceId); + } + + @Override + public void initDefaultCategories() { + updateCategoryList(""); } @Override @@ -463,14 +492,14 @@ private void configureRightCard() { private void configureNavigationButtons() { // Navigation next / previous for each image as we're collecting title + description - next.setOnClickListener(v -> presenter.handleNext()); + next.setOnClickListener(v -> presenter.handleNext(categoriesModel, false)); previous.setOnClickListener(v -> presenter.handlePrevious()); - // Next / previous for the category selection page - categoryNext.setOnClickListener(v -> presenter.handleNext()); + // Next / previous for the category selection currentPage + categoryNext.setOnClickListener(v -> presenter.handleNext(categoriesModel, false)); categoryPrevious.setOnClickListener(v -> presenter.handlePrevious()); - // Finally, the previous / submit buttons on the final page of the wizard + // Finally, the previous / submit buttons on the final currentPage of the wizard licensePrevious.setOnClickListener(v -> presenter.handlePrevious()); submit.setOnClickListener(v -> { Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG).show(); @@ -480,21 +509,15 @@ private void configureNavigationButtons() { } - private void configureCategories(Bundle savedInstanceState) { - ArrayList items = new ArrayList<>(); - if (savedInstanceState != null) { - items.addAll(savedInstanceState.getParcelableArrayList("currentCategories")); - //noinspection unchecked - categoriesModel.cacheAll((HashMap>) savedInstanceState - .getSerializable("categoriesCache")); - } - categoriesAdapter = new UploadCategoriesAdapterFactory(categoriesModel).create(items); + private void configureCategories() { + 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(); Observable.fromIterable(categoriesModel.getSelectedCategories()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -505,10 +528,10 @@ private void updateCategoryList(String filter) { }) .observeOn(Schedulers.io()) .concatWith( - categoriesModel.searchAll(filter) - .mergeWith(categoriesModel.searchCategories(filter)) + categoriesModel.searchAll(filter, imageTitleList) + .mergeWith(categoriesModel.searchCategories(filter, imageTitleList)) .concatWith(TextUtils.isEmpty(filter) - ? categoriesModel.defaultCategories() : Observable.empty()) + ? categoriesModel.defaultCategories(imageTitleList) : Observable.empty()) ) .filter(categoryItem -> !categoriesModel.containsYear(categoryItem.getName())) .distinct() @@ -574,18 +597,10 @@ public List getDescriptions() { } private void initRecyclerView() { - descriptionsAdapter = new DescriptionsAdapter(); + descriptionsAdapter = new DescriptionsAdapter(this); descriptionsAdapter.setCallback(this::showInfoAlert); rvDescriptions.setLayoutManager(new LinearLayoutManager(getApplicationContext())); rvDescriptions.setAdapter(descriptionsAdapter); - compositeDisposable.add( - descriptionsAdapter.getTitleChangeObserver() - .debounce(1000, TimeUnit.MILLISECONDS) - .observeOn(Schedulers.io()) - .filter(title -> mwApi.fileExistsWithName(title + "." + presenter.getCurrentItem().fileExt)) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(title -> showDuplicateTitlePopup(title + "." + presenter.getCurrentItem().fileExt), Timber::e) - ); } 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 7cf2915829a..a643c78ad55 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 @@ -25,6 +25,7 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.utils.ImageUtils; +import fr.free.nrw.commons.utils.StringUtils; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.disposables.Disposable; @@ -304,11 +305,6 @@ public void subscribeBadPicture(Consumer consumer) { badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e); } - public boolean isLoggedIn() { - Account currentAccount = sessionManager.getCurrentAccount(); - return currentAccount != null; - } - @SuppressWarnings("WeakerAccess") static class UploadItem { @@ -340,7 +336,6 @@ static class UploadItem { this.gpsCoords = gpsCoords; this.fileExt = fileExt; imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); -// imageQuality.subscribe(iq->Timber.i("New value of imageQuality:"+ImageUtils.IMAGE_OK)); } public boolean isDummy() { 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 a581e6e2d96..b0c063a6303 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 @@ -5,20 +5,31 @@ import android.os.Bundle; import java.lang.reflect.Proxy; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; +import fr.free.nrw.commons.R; import fr.free.nrw.commons.category.CategoriesModel; import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.utils.ImageUtils; import io.reactivex.Completable; +import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; 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; + /** * The MVP pattern presenter of Upload GUI */ @@ -29,15 +40,24 @@ public class UploadPresenter { private final UploadModel uploadModel; private final UploadController uploadController; + private final MediaWikiApi mediaWikiApi; + + private CompositeDisposable compositeDisposable; + private static final UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(), new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null); private UploadView view = DUMMY; + @UploadView.UploadPage int currentPage = UploadView.PLEASE_WAIT; + @Inject - public UploadPresenter(UploadModel uploadModel, UploadController uploadController) { + public UploadPresenter(UploadModel uploadModel, + UploadController uploadController, + MediaWikiApi mediaWikiApi) { this.uploadModel = uploadModel; this.uploadController = uploadController; + this.mediaWikiApi = mediaWikiApi; } public void receive(Uri mediaUri, String mimeType, String source) { @@ -100,27 +120,94 @@ public void selectLicense(String licenseName) { /** * Called by the next button in {@link UploadActivity} */ - public void handleNext() { + @SuppressLint("CheckResult") + public void handleNext(CategoriesModel categoriesModel, boolean noCategoryWarningShown) { + if(currentPage == UploadView.TITLE_CARD) { + validateCurrentItemTitle() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::handleImage); + } else if(currentPage == UploadView.CATEGORIES) { + if (categoriesModel.selectedCategoriesCount() < 1 && !noCategoryWarningShown) { + view.showNoCategorySelectedWarning(); + } else { + nextUploadedItem(); + } + } else { + nextUploadedItem(); + } + } + + private void handleImage(Integer errorCode) { + switch (errorCode) { + case EMPTY_TITLE: + view.showErrorMessage(R.string.add_title_toast); + break; + case FILE_NAME_EXISTS: + if(getCurrentItem().imageQuality.getValue().equals(IMAGE_KEEP)) { + nextUploadedItem(); + } else { + view.showBadPicturePopup(FILE_NAME_EXISTS); + } + break; + case IMAGE_OK: + default: + nextUploadedItem(); + } + } + + private void nextUploadedItem() { uploadModel.next(); updateContent(); - if (uploadModel.isShowingItem()) uploadModel.subscribeBadPicture(this::handleBadPicture); + if (uploadModel.isShowingItem()) { + uploadModel.subscribeBadPicture(this::handleBadPicture); + } view.dismissKeyboard(); } + private Title getCurrentImageTitle() { + return getCurrentItem().title; + } + + public String getCurrentImageFileName() { + UploadItem currentItem = getCurrentItem(); + return currentItem.title + "." + uploadModel.getCurrentItem().fileExt; + } + + @SuppressLint("CheckResult") + private Observable validateCurrentItemTitle() { + Title title = getCurrentImageTitle(); + if (title.isEmpty()) { + view.showErrorMessage(R.string.add_title_toast); + return Observable.just(EMPTY_TITLE); + } + + return Observable.fromCallable(() -> mediaWikiApi.fileExistsWithName(getCurrentImageFileName())) + .subscribeOn(Schedulers.io()) + .map(doesFileExist -> { + if (doesFileExist) { + return FILE_NAME_EXISTS; + } + return IMAGE_OK; + }); + } + /** * Called by the previous button in {@link UploadActivity} */ public void handlePrevious() { uploadModel.previous(); updateContent(); - if (uploadModel.isShowingItem()) uploadModel.subscribeBadPicture(this::handleBadPicture); + if (uploadModel.isShowingItem()) { + uploadModel.subscribeBadPicture(this::handleBadPicture); + } view.dismissKeyboard(); } /** * Called when one of the pictures on the top card is clicked on in {@link UploadActivity} */ - public void thumbnailClicked(UploadModel.UploadItem item) { + public void thumbnailClicked(UploadItem item) { uploadModel.jumpTo(item); updateContent(); } @@ -276,10 +363,10 @@ private void updateLicenses() { } /** - * Updates the cards and the background when a new page is selected. + * Updates the cards and the background when a new currentPage is selected. */ private void updateContent() { - Timber.i("Updating content for page" + uploadModel.getCurrentStep()); + Timber.i("Updating content for currentPage" + uploadModel.getCurrentStep()); view.setNextEnabled(uploadModel.isNextAvailable()); view.setPreviousEnabled(uploadModel.isPreviousAvailable()); view.setSubmitEnabled(uploadModel.isSubmitAvailable()); @@ -303,22 +390,22 @@ private void updateContent() { * @param uploadCount how many items are being uploaded */ private void showCorrectCards(int currentStep, int uploadCount) { - @UploadView.UploadPage int page; if (uploadCount == 0) { - page = UploadView.PLEASE_WAIT; + currentPage = UploadView.PLEASE_WAIT; } else if (currentStep <= uploadCount) { - page = UploadView.TITLE_CARD; + currentPage = UploadView.TITLE_CARD; view.setTopCardVisibility(uploadModel.getCount() > 1); } else if (currentStep == uploadCount + 1) { - page = UploadView.CATEGORIES; + currentPage = UploadView.CATEGORIES; view.setTopCardVisibility(false); view.setRightCardVisibility(false); + view.initDefaultCategories(); } else { - page = UploadView.LICENSE; + currentPage = UploadView.LICENSE; view.setTopCardVisibility(false); view.setRightCardVisibility(false); } - view.setBottomCardVisibility(page); + view.setBottomCardVisibility(currentPage); } //endregion @@ -326,8 +413,18 @@ private void showCorrectCards(int currentStep, int uploadCount) { /** * @return the item currently being displayed */ - public UploadModel.UploadItem getCurrentItem() { + public UploadItem getCurrentItem() { return uploadModel.getCurrentItem(); } + public List getImageTitleList() { + List titleList = new ArrayList<>(); + for (UploadItem item : uploadModel.getUploads()) { + if (item.title.isSet()) { + titleList.add(item.title.toString()); + } + } + return titleList; + } + } \ No newline at end of file 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 0256f64837a..47e434c72fe 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 @@ -71,4 +71,10 @@ public interface UploadView { void finish(); void launchMapActivity(String decCoords); + + void showErrorMessage(int resourceId); + + void initDefaultCategories(); + + void showNoCategorySelectedWarning(); } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java index a279179286d..65b1133571f 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java @@ -39,6 +39,9 @@ public class ImageUtils { public static final int IMAGE_OK = 0; public static final int IMAGE_KEEP = -1; public static final int IMAGE_WAIT = -2; + public static final int EMPTY_TITLE = -3; + public static final int FILE_NAME_EXISTS = -4; + public static final int NO_CATEGORY_SELECTED = -5; @IntDef( flag = true, @@ -48,7 +51,10 @@ public class ImageUtils { IMAGE_DUPLICATE, IMAGE_OK, IMAGE_KEEP, - IMAGE_WAIT + IMAGE_WAIT, + EMPTY_TITLE, + FILE_NAME_EXISTS, + NO_CATEGORY_SELECTED } ) @Retention(RetentionPolicy.SOURCE) diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java index 0eb8216e4cd..0f93e65ef85 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java @@ -12,4 +12,8 @@ public static String getParsedStringFromHtml(String source) { return Html.fromHtml(source).toString(); } } + + public static boolean isNullOrWhiteSpace(String value) { + return value == null || value.trim().isEmpty(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java index ed6513ca252..cbc17045969 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java @@ -2,6 +2,7 @@ import android.app.Activity; import android.content.Context; +import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; import android.view.Display; import android.view.View; @@ -30,6 +31,30 @@ public static void showLongToast(Context context, String text) { ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show()); } + public static void showLongToast(Context context, @StringRes int stringResourceId) { + if (context == null) { + return; + } + + ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show()); + } + + public static void showShortToast(Context context, String text) { + if (context == null) { + return; + } + + ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_SHORT).show()); + } + + public static void showShortToast(Context context, @StringRes int stringResourceId) { + if (context == null) { + return; + } + + ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show()); + } + public static boolean isPortrait(Context context) { Display orientation = ((Activity)context).getWindowManager().getDefaultDisplay(); if (orientation.getWidth() < orientation.getHeight()){ diff --git a/app/src/main/res/layout/activity_upload_bottom_card.xml b/app/src/main/res/layout/activity_upload_bottom_card.xml index 5d1d7d7567e..273835b5d66 100644 --- a/app/src/main/res/layout/activity_upload_bottom_card.xml +++ b/app/src/main/res/layout/activity_upload_bottom_card.xml @@ -19,7 +19,7 @@ + + + app:layout_constraintTop_toBottomOf="@+id/bottom_card_subtitle" + tools:visibility="gone"/>