From bd9531b969d843edf00cfe72c97b7f3a12071e26 Mon Sep 17 00:00:00 2001 From: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> Date: Tue, 22 Mar 2022 11:03:43 +0530 Subject: [PATCH] Fixed 4616 : Option for editing depictions (#4725) * Dialog can't be dismissed * Dialog can't be dismissed * Option for editing depiction * Java docs added * Minor issues fixed * Lining done * "Depictions not updating instantly" issue resolved * Existing Depicts on the top * Existing Depicts on the top * Back press handled * Previous depictions unchecked * Whole Screen issue fixed * Nearby banner removed * Test fixed * Upload Wizard issue fixed * Upload Wizard issue fixed * Previous depicts issue fixed * Previous depicts issue fixed * All issues fixed * Fixed late loading of updated depicts * Depiction is removable * Test fixed * Back button press handled after losing focus for edittext * RequiresApi removed * RequiresApi removed * Test fixed * Requested changes * Test added * Test added * UploadModelUnitTest added * DepictEditHelperUnitTest added * DepictEditHelperUnitTest added * Test added * More test added * Indentation Reversed * Indentation reversed * Update MediaDetailFragment.java * Indentation reversed * Update MediaDetailFragment.java * Indentation reversed * Indentation reversed * Indentation reversed * Indentation reversed * More test added * More test added * Minor fixes * Minor fixes * Minor fixes --- .../commons/contributions/MainActivity.java | 10 + .../commons/media/MediaDetailFragment.java | 43 +++- .../notification/NotificationHelper.java | 1 + .../commons/repository/UploadRepository.java | 55 +++- .../free/nrw/commons/upload/UploadModel.java | 53 +++- .../nrw/commons/upload/WikiBaseInterface.java | 15 ++ .../upload/depicts/DepictEditHelper.kt | 121 +++++++++ .../upload/depicts/DepictsContract.java | 48 ++++ .../upload/depicts/DepictsFragment.java | 242 ++++++++++++++++-- .../upload/depicts/DepictsPresenter.kt | 120 ++++++++- .../nrw/commons/wikidata/WikiBaseClient.java | 16 +- .../commons/wikidata/WikidataEditService.java | 55 +++- .../main/res/layout/fragment_media_detail.xml | 9 + .../res/layout/upload_depicts_fragment.xml | 4 +- app/src/main/res/values/strings.xml | 9 + .../media/MediaDetailFragmentUnitTests.kt | 18 +- .../commons/upload/DepictsPresenterTest.kt | 73 +++++- .../nrw/commons/upload/UploadModelUnitTest.kt | 126 +++++++++ .../upload/UploadRepositoryUnitTest.kt | 51 +++- .../depicts/DepictEditHelperUnitTest.kt | 109 ++++++++ .../depicts/DepictsFragmentUnitTests.kt | 102 ++++++++ .../wikidata/WikiBaseClientUnitTest.kt | 31 +++ .../wikidata/WikidataEditServiceTest.kt | 9 + .../java/org/wikipedia/wikidata/EditClaim.kt | 16 +- 24 files changed, 1261 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictEditHelper.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/upload/UploadModelUnitTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/upload/depicts/DepictEditHelperUnitTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/wikidata/WikiBaseClientUnitTest.kt diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 23cc2eb4f..afa377c2f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -125,6 +125,16 @@ public class MainActivity extends BaseActivity toolbar.setNavigationOnClickListener(view -> { onSupportNavigateUp(); }); + /* + "first_edit_depict" is a key for getting information about opening the depiction editor + screen for the first time after opening the app. + + Getting true by the key means the depiction editor screen is opened for the first time + after opening the app. + Getting false by the key means the depiction editor screen is not opened for the first time + after opening the app. + */ + applicationKvStore.putBoolean("first_edit_depict", true); if (applicationKvStore.getBoolean("login_skipped") == true) { setTitle(getString(R.string.navigation_item_explore)); setUpLoggedOutPager(); 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 fc0b49df3..d0fa6dd2a 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 @@ -10,10 +10,8 @@ import static fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_D import static fr.free.nrw.commons.description.EditDescriptionConstants.UPDATED_WIKITEXT; import static fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT; import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION; -import android.content.res.Resources; import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources; import android.annotation.SuppressLint; -import java.lang.reflect.Field; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; @@ -45,6 +43,8 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import butterknife.BindView; @@ -86,6 +86,7 @@ import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.nearby.Label; import fr.free.nrw.commons.profile.ProfileActivity; import fr.free.nrw.commons.ui.widget.HtmlTextView; +import fr.free.nrw.commons.upload.depicts.DepictsFragment; import fr.free.nrw.commons.upload.UploadMediaDetail; import fr.free.nrw.commons.utils.ViewUtilWrapper; import io.reactivex.Single; @@ -93,7 +94,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; @@ -176,6 +176,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements LinearLayout captionLayout; @BindView(R.id.depicts_layout) LinearLayout depictsLayout; + @BindView(R.id.depictionsEditButton) + Button depictEditButton; @BindView(R.id.media_detail_caption) TextView mediaCaption; @BindView(R.id.mediaDetailDesc) @@ -239,7 +241,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements @BindView(R.id.description_label) TextView descriptionLabel; @BindView(R.id.pb_circular) - ProgressBar progressBar; + ProgressBar progressBar; String descriptionHtmlCode; @BindView(R.id.progressBarDeletion) ProgressBar progressBarDeletion; @@ -467,10 +469,10 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements private void displayMediaDetails() { setTextFields(media); compositeDisposable.addAll( - mediaDataExtractor.fetchDepictionIdsAndLabels(media) + mediaDataExtractor.refresh(media) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onDepictionsLoaded, Timber::e), + .subscribe(this::onMediaRefreshed, Timber::e), mediaDataExtractor.checkDeletionRequestExists(media) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -478,15 +480,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements mediaDataExtractor.fetchDiscussion(media) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onDiscussionLoaded, Timber::e), - mediaDataExtractor.refresh(media) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onMediaRefreshed, Timber::e) + .subscribe(this::onDiscussionLoaded, Timber::e) ); } private void onMediaRefreshed(Media media) { + this.media = media; setTextFields(media); compositeDisposable.addAll( mediaDataExtractor.fetchDepictionIdsAndLabels(media) @@ -517,8 +516,26 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements } private void onDepictionsLoaded(List idAndCaptions){ - depictsLayout.setVisibility(idAndCaptions.isEmpty() ? GONE : VISIBLE); - buildDepictionList(idAndCaptions); + depictsLayout.setVisibility(idAndCaptions.isEmpty() ? GONE : VISIBLE); + depictEditButton.setVisibility(idAndCaptions.isEmpty() ? GONE : VISIBLE); + buildDepictionList(idAndCaptions); + } + + /** + * By clicking on the edit depictions button, it will send user to depict fragment + */ + @OnClick(R.id.depictionsEditButton) + public void onDepictionsEditButtonClicked() { + depictionContainer.removeAllViews(); + depictEditButton.setVisibility(GONE); + final Fragment depictsFragment = new DepictsFragment(); + final Bundle bundle = new Bundle(); + bundle.putParcelable("Existing_Depicts", media); + depictsFragment.setArguments(bundle); + final FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); + transaction.replace(R.id.mediaDetailFrameLayout, depictsFragment); + transaction.addToBackStack(null); + transaction.commit(); } /** * The imageSpacer is Basically a transparent overlay for the SimpleDraweeView diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java index f7e3592a0..37d13f92c 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java @@ -27,6 +27,7 @@ public class NotificationHelper { public static final int NOTIFICATION_EDIT_CATEGORY = 2; public static final int NOTIFICATION_EDIT_COORDINATES = 3; public static final int NOTIFICATION_EDIT_DESCRIPTION = 4; + public static final int NOTIFICATION_EDIT_DEPICTIONS = 5; private NotificationManager notificationManager; private NotificationCompat.Builder notificationBuilder; diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java index f91519ef8..cba71ba83 100644 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.repository; import androidx.annotation.Nullable; +import fr.free.nrw.commons.Media; import fr.free.nrw.commons.category.CategoriesModel; import fr.free.nrw.commons.category.CategoryItem; import fr.free.nrw.commons.contributions.Contribution; @@ -233,8 +234,8 @@ public class UploadRepository { uploadModel.setSelectedLicense(licenseName); } - public void onDepictItemClicked(DepictedItem depictedItem) { - uploadModel.onDepictItemClicked(depictedItem); + public void onDepictItemClicked(DepictedItem depictedItem, final Media media) { + uploadModel.onDepictItemClicked(depictedItem, media); } /** @@ -247,6 +248,23 @@ public class UploadRepository { return uploadModel.getSelectedDepictions(); } + /** + * Provides selected existing depicts + * + * @return selected existing depicts + */ + public List getSelectedExistingDepictions() { + return uploadModel.getSelectedExistingDepictions(); + } + + /** + * Initialize existing depicts + * + * @param selectedExistingDepictions existing depicts + */ + public void setSelectedExistingDepictions(final List selectedExistingDepictions) { + uploadModel.setSelectedExistingDepictions(selectedExistingDepictions); + } /** * Search all depictions from * @@ -275,6 +293,39 @@ public class UploadRepository { return depictModel.getPlaceDepictions(new ArrayList<>(qids)); } + /** + * Takes depict IDs as a parameter, converts into a slash separated String and Gets DepictItem + * from the server + * + * @param depictionsQIDs IDs of Depiction + * @return Flowable> + */ + public Flowable> getDepictions(final List depictionsQIDs){ + final String ids = joinQIDs(depictionsQIDs); + return depictModel.getDepictions(ids).toFlowable(); + } + + /** + * Builds a string by joining all IDs divided by "|" + * + * @param depictionsQIDs IDs of depiction ex. ["Q11023","Q1356"] + * @return string ex. "Q11023|Q1356" + */ + private String joinQIDs(final List depictionsQIDs) { + if (depictionsQIDs != null && !depictionsQIDs.isEmpty()) { + final StringBuilder buffer = new StringBuilder(depictionsQIDs.get(0)); + + if (depictionsQIDs.size() > 1) { + for (int i = 1; i < depictionsQIDs.size(); i++) { + buffer.append("|"); + buffer.append(depictionsQIDs.get(i)); + } + } + return buffer.toString(); + } + return null; + } + /** * Returns nearest place matching the passed latitude and longitude * @param decLatitude 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 48da67394..e2c562414 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 @@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload; import android.content.Context; import android.net.Uri; +import fr.free.nrw.commons.Media; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.filepicker.UploadableFile; @@ -40,6 +41,10 @@ public class UploadModel { private final ImageProcessingService imageProcessingService; private final List selectedCategories = new ArrayList<>(); private final List selectedDepictions = new ArrayList<>(); + /** + * Existing depicts which are selected + */ + private List selectedExistingDepictions = new ArrayList<>(); @Inject UploadModel(@Named("licenses") final List licenses, @@ -68,6 +73,7 @@ public class UploadModel { items.clear(); selectedCategories.clear(); selectedDepictions.clear(); + selectedExistingDepictions.clear(); } public void setSelectedCategories(List selectedCategories) { @@ -185,11 +191,33 @@ public class UploadModel { return items; } - public void onDepictItemClicked(DepictedItem depictedItem) { - if (depictedItem.isSelected()) { - selectedDepictions.add(depictedItem); + public void onDepictItemClicked(DepictedItem depictedItem, Media media) { + if (media == null) { + if (depictedItem.isSelected()) { + selectedDepictions.add(depictedItem); + } else { + selectedDepictions.remove(depictedItem); + } } else { - selectedDepictions.remove(depictedItem); + if (depictedItem.isSelected()) { + if (media.getDepictionIds().contains(depictedItem.getId())) { + selectedExistingDepictions.add(depictedItem.getId()); + } else { + selectedDepictions.add(depictedItem); + } + } else { + if (media.getDepictionIds().contains(depictedItem.getId())) { + selectedExistingDepictions.remove(depictedItem.getId()); + if (!media.getDepictionIds().contains(depictedItem.getId())) { + final List depictsList = new ArrayList<>(); + depictsList.add(depictedItem.getId()); + depictsList.addAll(media.getDepictionIds()); + media.setDepictionIds(depictsList); + } + } else { + selectedDepictions.remove(depictedItem); + } + } } } @@ -207,4 +235,21 @@ public class UploadModel { return selectedDepictions; } + /** + * Provides selected existing depicts + * + * @return selected existing depicts + */ + public List getSelectedExistingDepictions() { + return selectedExistingDepictions; + } + + /** + * Initialize existing depicts + * + * @param selectedExistingDepictions existing depicts + */ + public void setSelectedExistingDepictions(final List selectedExistingDepictions) { + this.selectedExistingDepictions = selectedExistingDepictions; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/WikiBaseInterface.java b/app/src/main/java/fr/free/nrw/commons/upload/WikiBaseInterface.java index 79ba57bc7..ccb3f2e60 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/WikiBaseInterface.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/WikiBaseInterface.java @@ -26,6 +26,21 @@ public interface WikiBaseInterface { @NonNull @Field("token") String editToken, @NonNull @Field("data") String data); + /** + * Uploads depicts for a file in the server + * + * @param filename name of the file + * @param editToken editToken for the file + * @param data data of the depicts to be uploaded + * @return Observable + */ + @Headers("Cache-Control: no-cache") + @FormUrlEncoded + @POST(MW_API_PREFIX + "action=wbeditentity&site=commonswiki&clear=1") + Observable postEditEntityByFilename(@NonNull @Field("title") String filename, + @NonNull @Field("token") String editToken, + @NonNull @Field("data") String data); + @GET(MW_API_PREFIX + "action=query&prop=info") Observable getFileEntityId(@Query("titles") String fileName); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictEditHelper.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictEditHelper.kt new file mode 100644 index 000000000..92cd29cbf --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictEditHelper.kt @@ -0,0 +1,121 @@ +package fr.free.nrw.commons.upload.depicts + +import android.content.Context +import android.content.Intent +import android.net.Uri +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.notification.NotificationHelper +import fr.free.nrw.commons.utils.ViewUtilWrapper +import fr.free.nrw.commons.wikidata.WikidataEditService +import io.reactivex.Observable +import timber.log.Timber +import javax.inject.Inject + +class DepictEditHelper @Inject constructor (notificationHelper: NotificationHelper, + wikidataEditService: WikidataEditService, + viewUtilWrapper: ViewUtilWrapper) { + + /** + * Class for making post operations + */ + @Inject + lateinit var wikidataEditService: WikidataEditService + + /** + * Class for creating notification + */ + @Inject + lateinit var notificationHelper: NotificationHelper + + /** + * Class for showing toast + */ + @Inject + lateinit var viewUtilWrapper: ViewUtilWrapper + + /** + * Public interface to edit depictions + * + * @param context context + * @param media media + * @param depictions selected depictions to be added ex: ["Q12", "Q234"] + * @return Single + */ + fun makeDepictionEdit( + context: Context, + media: Media, + depictions: List + ): Observable { + viewUtilWrapper.showShortToast( + context, + context.getString(R.string.depictions_edit_helper_make_edit_toast) + ) + return addDepiction(media, depictions) + .flatMap { result: Boolean -> + Observable.just( + showDepictionEditNotification(context, media, result) + ) + } + } + + /** + * Appends new depictions + * + * @param media media + * @param depictions to be added + * @return Observable + */ + private fun addDepiction(media: Media, depictions: List): Observable { + Timber.d("thread is adding depiction %s", Thread.currentThread().name) + return wikidataEditService.updateDepictsProperty(media.filename, depictions) + } + + /** + * Helps to create notification about condition of editing depictions + * + * @param context context + * @param media media + * @param result response of result + * @return Single + */ + private fun showDepictionEditNotification( + context: Context, + media: Media, + result: Boolean + ): Boolean { + val message: String + var title = context.getString(R.string.depictions_edit_helper_show_edit_title) + if (result) { + title += ": " + context.getString(R.string.category_edit_helper_show_edit_title_success) + val depictsInMessage = StringBuilder() + val depictIdList = media.depictionIds + for (depiction in depictIdList) { + depictsInMessage.append(depiction) + if (depiction == depictIdList[depictIdList.size - 1]) { + continue + } + depictsInMessage.append(",") + } + message = context.resources.getQuantityString( + R.plurals.depictions_edit_helper_show_edit_message_if, + depictIdList.size, + depictsInMessage.toString() + ) + } else { + title += ": " + context.getString(R.string.depictions_edit_helper_show_edit_title) + message = context.getString(R.string.depictions_edit_helper_edit_message_else) + } + val urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.filename + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile)) + notificationHelper.showNotification( + context, + title, + message, + NotificationHelper.NOTIFICATION_EDIT_DEPICTIONS, + browserIntent + ) + return result + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.java b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.java index 0184744f8..1705330cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.java @@ -1,7 +1,10 @@ package fr.free.nrw.commons.upload.depicts; +import android.content.Context; +import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import fr.free.nrw.commons.BasePresenter; +import fr.free.nrw.commons.Media; import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; import java.util.List; @@ -40,6 +43,36 @@ public interface DepictsContract { * add depictions to list */ void setDepictsList(List depictedItemList); + + /** + * Returns required context + */ + Context getFragmentContext(); + + /** + * Returns to previous fragment + */ + void goBackToPreviousScreen(); + + /** + * Gets existing depictions IDs from media + */ + List getExistingDepictions(); + + /** + * Shows the progress dialog + */ + void showProgressDialog(); + + /** + * Hides the progress dialog + */ + void dismissProgressDialog(); + + /** + * Update the depictions + */ + void updateDepicts(); } interface UserActionListener extends BasePresenter { @@ -71,6 +104,21 @@ public interface DepictsContract { */ void verifyDepictions(); + /** + * Clears previous selections + */ + void clearPreviousSelection(); + LiveData> getDepictedItems(); + + /** + * Update the depictions + */ + void updateDepictions(Media media); + + /** + * Attaches view and media + */ + void onAttachViewWithMedia(@NonNull View view, Media media); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java index 493d5749d..25ccd0015 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java @@ -1,15 +1,21 @@ package fr.free.nrw.commons.upload.depicts; import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; import android.os.Bundle; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import butterknife.BindView; @@ -18,7 +24,11 @@ import butterknife.OnClick; import com.google.android.material.textfield.TextInputLayout; import com.jakewharton.rxbinding2.view.RxView; import com.jakewharton.rxbinding2.widget.RxTextView; +import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.ContributionsFragment; +import fr.free.nrw.commons.kvstore.JsonKvStore; +import fr.free.nrw.commons.media.MediaDetailFragment; import fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText; import fr.free.nrw.commons.upload.UploadActivity; import fr.free.nrw.commons.upload.UploadBaseFragment; @@ -27,8 +37,10 @@ import fr.free.nrw.commons.utils.DialogUtil; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import java.util.List; +import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.inject.Inject; +import javax.inject.Named; import kotlin.Unit; import timber.log.Timber; @@ -52,11 +64,26 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra RecyclerView depictsRecyclerView; @BindView(R.id.tooltip) ImageView tooltip; + @BindView(R.id.depicts_next) + Button btnNext; + @BindView(R.id.depicts_previous) + Button btnPrevious; + @Inject + @Named("default_preferences") + public + JsonKvStore applicationKvStore; @Inject DepictsContract.UserActionListener presenter; private UploadDepictsAdapter adapter; private Disposable subscribe; + private Media media; + private ProgressDialog progressDialog; + /** + * Determines each encounter of edit depicts + */ + private int count; + @Nullable @Override public android.view.View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @@ -67,7 +94,13 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra @Override public void onViewCreated(@NonNull android.view.View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + ButterKnife.bind(this, view); + Bundle bundle = getArguments(); + if (bundle != null) { + media = bundle.getParcelable("Existing_Depicts"); + } + init(); presenter.getDepictedItems().observe(getViewLifecycleOwner(), this::setDepictsList); } @@ -76,13 +109,27 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra * Initialize presenter and views */ private void init() { - depictsTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1, - callback.getTotalNumberOfSteps(), getString(R.string.depicts_step_title))); + + if (media == null) { + depictsTitle + .setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1, + callback.getTotalNumberOfSteps(), getString(R.string.depicts_step_title))); + } else { + depictsTitle.setText(R.string.edit_depictions); + depictsSubTitle.setVisibility(View.GONE); + btnNext.setText(R.string.menu_save_categories); + btnPrevious.setText(R.string.menu_cancel_upload); + } + setDepictsSubTitle(); tooltip.setOnClickListener(v -> DialogUtil .showAlertDialog(getActivity(), getString(R.string.depicts_step_title), getString(R.string.depicts_tooltip), getString(android.R.string.ok), null, true)); - presenter.onAttachView(this); + if (media == null) { + presenter.onAttachView(this); + } else { + presenter.onAttachViewWithMedia(this, media); + } initRecyclerView(); addTextChangeListenerToSearchBox(); } @@ -105,10 +152,17 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra * Initialise recyclerView and set adapter */ private void initRecyclerView() { - adapter = new UploadDepictsAdapter(item -> { - presenter.onDepictItemClicked(item); - return Unit.INSTANCE; - }); + if (media == null) { + adapter = new UploadDepictsAdapter(categoryItem -> { + presenter.onDepictItemClicked(categoryItem); + return Unit.INSTANCE; + }); + } else { + adapter = new UploadDepictsAdapter(item -> { + presenter.onDepictItemClicked(item); + return Unit.INSTANCE; + }); + } depictsRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); depictsRecyclerView.setAdapter(adapter); } @@ -133,19 +187,28 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra @Override public void noDepictionSelected() { - DialogUtil.showAlertDialog(getActivity(), - getString(R.string.no_depictions_selected), - getString(R.string.no_depictions_selected_warning_desc), - getString(R.string.continue_message), - getString(R.string.cancel), - this::goToNextScreen, - null - ); + if (media == null) { + DialogUtil.showAlertDialog(getActivity(), + getString(R.string.no_depictions_selected), + getString(R.string.no_depictions_selected_warning_desc), + getString(R.string.continue_message), + getString(R.string.cancel), + this::goToNextScreen, + null + ); + } else { + Toast.makeText(requireContext(), getString(R.string.no_depictions_selected), + Toast.LENGTH_SHORT).show(); + presenter.clearPreviousSelection(); + updateDepicts(); + goBackToPreviousScreen(); + } } @Override public void onDestroyView() { super.onDestroyView(); + media = null; presenter.onDetachView(); subscribe.dispose(); } @@ -166,18 +229,98 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra @Override public void setDepictsList(List depictedItemList) { - adapter.setItems(depictedItemList); + + if (applicationKvStore.getBoolean("first_edit_depict")) { + count = 1; + applicationKvStore.putBoolean("first_edit_depict", false); + adapter.setItems(depictedItemList); + } else { + if ((count == 0) && (!depictedItemList.isEmpty())) { + adapter.setItems(null); + count = 1; + } else { + adapter.setItems(depictedItemList); + } + } depictsRecyclerView.smoothScrollToPosition(0); } - @OnClick(R.id.depicts_next) - public void onNextButtonClicked() { - presenter.verifyDepictions(); + /** + * Returns required context + */ + @Override + public Context getFragmentContext(){ + return requireContext(); } + /** + * Returns to previous fragment + */ + @Override + public void goBackToPreviousScreen() { + getFragmentManager().popBackStack(); + } + + /** + * Gets existing depictions IDs from media + */ + @Override + public List getExistingDepictions(){ + return (media == null) ? null : media.getDepictionIds(); + } + + /** + * Shows the progress dialog + */ + @Override + public void showProgressDialog() { + progressDialog = new ProgressDialog(requireContext()); + progressDialog.setMessage(getString(R.string.please_wait)); + progressDialog.show(); + } + + /** + * Hides the progress dialog + */ + @Override + public void dismissProgressDialog() { + progressDialog.dismiss(); + } + + /** + * Update the depicts + */ + @Override + public void updateDepicts() { + final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment(); + assert mediaDetailFragment != null; + mediaDetailFragment.onResume(); + } + + /** + * Determines the calling fragment by media nullability and act accordingly + */ + @OnClick(R.id.depicts_next) + public void onNextButtonClicked() { + if(media != null){ + presenter.updateDepictions(media); + } else { + presenter.verifyDepictions(); + } + } + + /** + * Determines the calling fragment by media nullability and act accordingly + */ @OnClick(R.id.depicts_previous) public void onPreviousButtonClicked() { - callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this)); + if(media != null){ + presenter.clearPreviousSelection(); + updateDepicts(); + goBackToPreviousScreen(); + } else { + callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this)); + } } /** @@ -200,4 +343,63 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra private void searchForDepictions(final String query) { presenter.searchForDepictions(query); } + + + + /** + * Hides the action bar while opening editing fragment + */ + @Override + public void onResume() { + super.onResume(); + + if (media != null) { + depictsSearch.setOnKeyListener((v, keyCode, event) -> { + if (keyCode == KeyEvent.KEYCODE_BACK) { + depictsSearch.clearFocus(); + presenter.clearPreviousSelection(); + updateDepicts(); + goBackToPreviousScreen(); + return true; + } + return false; + }); + + Objects.requireNonNull(getView()).setFocusableInTouchMode(true); + getView().requestFocus(); + getView().setOnKeyListener((v, keyCode, event) -> { + if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + presenter.clearPreviousSelection(); + updateDepicts(); + goBackToPreviousScreen(); + return true; + } + return false; + }); + + Objects.requireNonNull( + ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + .hide(); + + if (getParentFragment().getParentFragment().getParentFragment() + instanceof ContributionsFragment) { + ((ContributionsFragment) (getParentFragment() + .getParentFragment().getParentFragment())).nearbyNotificationCardView + .setVisibility(View.GONE); + } + } + } + + /** + * Shows the action bar while closing editing fragment + */ + @Override + public void onStop() { + super.onStop(); + if (media != null) { + Objects.requireNonNull( + ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + .show(); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt index 388dfeccf..a2cba9a52 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt @@ -1,15 +1,19 @@ package fr.free.nrw.commons.upload.depicts +import android.annotation.SuppressLint import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import fr.free.nrw.commons.Media import fr.free.nrw.commons.di.CommonsApplicationModule import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems import io.reactivex.Flowable import io.reactivex.Scheduler +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.processors.PublishProcessor +import io.reactivex.schedulers.Schedulers import timber.log.Timber import java.lang.reflect.Proxy import java.util.* @@ -35,8 +39,11 @@ class DepictsPresenter @Inject constructor( private val compositeDisposable: CompositeDisposable = CompositeDisposable() private val searchTerm: PublishProcessor = PublishProcessor.create() private val depictedItems: MutableLiveData> = MutableLiveData() + private var media: Media? = null @Inject - lateinit var depictsDao: DepictsDao; + lateinit var depictsDao: DepictsDao + @Inject + lateinit var depictsHelper: DepictEditHelper override fun onAttachView(view: DepictsContract.View) { this.view = view @@ -71,16 +78,37 @@ class DepictsPresenter @Inject constructor( if (querystring.isEmpty()) { recentDepictedItemList = getRecentDepictedItems(); } - return repository.searchAllEntities(querystring) - .subscribeOn(ioScheduler) - .map { repository.selectedDepictions + it + recentDepictedItemList } - .map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } } - .map { it.distinctBy(DepictedItem::id) } - } + if (media == null) { + return repository.searchAllEntities(querystring) + .subscribeOn(ioScheduler) + .map { repository.selectedDepictions + it + recentDepictedItemList } + .map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } } + .map { it.distinctBy(DepictedItem::id) } + + } else { + return Flowable.zip(repository.getDepictions(repository.selectedExistingDepictions) + .map { list -> list.map { + DepictedItem(it.name, it.description, it.imageUrl, it.instanceOfs, + it.commonsCategories, true, it.id) + } + }, + repository.searchAllEntities(querystring), + { it1, it2 -> + it1 + it2 + } + ) + .subscribeOn(ioScheduler) + .map { repository.selectedDepictions + it + recentDepictedItemList } + .map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } } + .map { it.distinctBy(DepictedItem::id) } + + } + } override fun onDetachView() { view = DUMMY + media = null compositeDisposable.clear() } @@ -102,7 +130,7 @@ class DepictsPresenter @Inject constructor( private fun selectNewDepictions(toSelect: List) { toSelect.forEach { it.isSelected = true - repository.onDepictItemClicked(it) + repository.onDepictItemClicked(it, media) } // Add the new selections to the list of depicted items so that the selections appear @@ -113,12 +141,16 @@ class DepictsPresenter @Inject constructor( ?.let { depictedItems.value = it } } + override fun clearPreviousSelection() { + repository.cleanup() + } + override fun onPreviousButtonClicked() { view.goToPreviousScreen() } override fun onDepictItemClicked(depictedItem: DepictedItem) { - repository.onDepictItemClicked(depictedItem) + repository.onDepictItemClicked(depictedItem, media) } override fun getDepictedItems(): LiveData> { @@ -149,6 +181,76 @@ class DepictsPresenter @Inject constructor( } } + /** + * Gets the selected depicts and send them for posting to the server + * and saves them in local storage + */ + @SuppressLint("CheckResult") + override fun updateDepictions(media: Media) { + if (repository.selectedDepictions.isNotEmpty() + || repository.selectedExistingDepictions.size != view.existingDepictions.size + ) { + view.showProgressDialog() + val selectedDepictions: MutableList = + (repository.selectedDepictions.map { it.id }.toMutableList() + + repository.selectedExistingDepictions).toMutableList() + + if (selectedDepictions.isNotEmpty()) { + if (::depictsDao.isInitialized) { + //save all the selected Depicted item in room Database + depictsDao.savingDepictsInRoomDataBase(repository.selectedDepictions) + } + + compositeDisposable.add( + depictsHelper.makeDepictionEdit(view.fragmentContext, media, selectedDepictions) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + media.depictionIds = selectedDepictions + repository.cleanup() + view.dismissProgressDialog() + view.updateDepicts() + view.goBackToPreviousScreen() + }) + { + Timber.e( + "Failed to update depictions" + ) + } + ) + + } + } else { + repository.cleanup() + view.noDepictionSelected() + } + } + + override fun onAttachViewWithMedia(view: DepictsContract.View, media: Media) { + this.view = view + this.media = media + repository.selectedExistingDepictions = view.existingDepictions + compositeDisposable.add( + searchTerm + .observeOn(mainThreadScheduler) + .doOnNext { view.showProgress(true) } + .switchMap(::searchResultsWithTerm) + .observeOn(mainThreadScheduler) + .subscribe( + { (results, term) -> + view.showProgress(false) + view.showError(results.isEmpty() && term.isNotEmpty()) + depictedItems.value = results + }, + { t: Throwable? -> + view.showProgress(false) + view.showError(true) + Timber.e(t) + } + ) + ) + } + /** * Get the depicts from DepictsRoomdataBase */ diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java index 0ffce9ee4..a2233a004 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java @@ -1,7 +1,7 @@ package fr.free.nrw.commons.wikidata; -import static fr.free.nrw.commons.media.MediaClientKt.PAGE_ID_PREFIX; import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF; +import static fr.free.nrw.commons.media.MediaClientKt.PAGE_ID_PREFIX; import fr.free.nrw.commons.upload.UploadResult; import fr.free.nrw.commons.upload.WikiBaseInterface; @@ -35,6 +35,20 @@ public class WikiBaseClient { .map(response -> (response.getSuccessVal() == 1))); } + /** + * Makes the server call for posting new depicts + * + * @param filename name of the file + * @param data data of the depicts to be uploaded + * @return Observable + */ + public Observable postEditEntityByFilename(final String filename, final String data) { + return csrfToken() + .switchMap(editToken -> wikiBaseInterface.postEditEntityByFilename(filename, + editToken, data) + .map(response -> (response.getSuccessVal() == 1))); + } + public Observable getFileEntityId(UploadResult uploadResult) { return wikiBaseInterface.getFileEntityId(uploadResult.createCanonicalFileName()) .map(response -> (long) (response.query().pages().get(0).pageId())); diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java index e283c98a8..7a2c75d65 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java @@ -16,7 +16,6 @@ import fr.free.nrw.commons.upload.WikidataPlace; import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; -import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import java.util.ArrayList; import java.util.Arrays; @@ -73,11 +72,12 @@ public class WikidataEditService { */ @SuppressLint("CheckResult") private Observable addDepictsProperty(final String fileEntityId, - final WikidataItem depictedItem) { + final List depictedItems) { final EditClaim data = editClaim( - ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10) - : depictedItem.getId() + ConfigUtils.isBetaFlavour() ? Collections.singletonList("Q10") + // Wikipedia:Sandbox (Q10) + : depictedItems ); return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data)) @@ -95,8 +95,42 @@ public class WikidataEditService { .subscribeOn(Schedulers.io()); } - private EditClaim editClaim(final String entityId) { - return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName()); + /** + * Takes depicts ID as a parameter and create a uploadable data with the Id + * and send the data for POST operation + * + * @param filename name of the file + * @param depictedItems ID of the selected depict item + * @return Observable + */ + @SuppressLint("CheckResult") + public Observable updateDepictsProperty(final String filename, + final List depictedItems) { + + final EditClaim data = editClaim( + ConfigUtils.isBetaFlavour() ? Collections.singletonList("Q10") + // Wikipedia:Sandbox (Q10) + : depictedItems + ); + + return wikiBaseClient.postEditEntityByFilename(filename, + gson.toJson(data)) + .doOnNext(success -> { + if (success) { + Timber.d("DEPICTS property was set successfully for %s", filename); + } else { + Timber.d("Unable to set DEPICTS property for %s", filename); + } + }) + .doOnError(throwable -> { + Timber.e(throwable, "Error occurred while setting DEPICTS property"); + ViewUtil.showLongToast(context, throwable.toString()); + }) + .subscribeOn(Schedulers.io()); + } + + private EditClaim editClaim(final List entityIds) { + return EditClaim.from(entityIds, WikidataProperties.DEPICTS.getPropertyName()); } /** @@ -209,7 +243,12 @@ public class WikidataEditService { } private Observable depictionEdits(Contribution contribution, Long fileEntityId) { - return Observable.fromIterable(contribution.getDepictedItems()) - .concatMap(wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem)); + final List depictIDs = new ArrayList<>(); + for (final WikidataItem wikidataItem : + contribution.getDepictedItems()) { + depictIDs.add(wikidataItem.getId()); + } + return addDepictsProperty(fileEntityId.toString(), depictIDs); } } + diff --git a/app/src/main/res/layout/fragment_media_detail.xml b/app/src/main/res/layout/fragment_media_detail.xml index 393981e1c..f397be1fb 100644 --- a/app/src/main/res/layout/fragment_media_detail.xml +++ b/app/src/main/res/layout/fragment_media_detail.xml @@ -336,6 +336,15 @@ android:orientation="vertical" /> +