Migration of review module from Java to Kotlin (#5950)

* Rename .java to .kt

* Migrated repository module to Kotlin

* Rename .java to .kt

* Migrated review module to Kotlin
This commit is contained in:
Saifuddin Adenwala 2024-11-23 18:15:46 +05:30 committed by GitHub
parent e070c5dbe8
commit bafae821e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 906 additions and 932 deletions

View file

@ -232,7 +232,7 @@ public class MoreBottomSheetFragment extends BottomSheetDialogFragment {
}
protected void onPeerReviewClicked() {
ReviewActivity.startYourself(getActivity(), getString(R.string.title_activity_review));
ReviewActivity.Companion.startYourself(getActivity(), getString(R.string.title_activity_review));
}
}

View file

@ -1,334 +0,0 @@
package fr.free.nrw.commons.review;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.databinding.ActivityReviewBinding;
import fr.free.nrw.commons.delete.DeleteHelper;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Locale;
import javax.inject.Inject;
public class ReviewActivity extends BaseActivity {
private ActivityReviewBinding binding;
MediaDetailFragment mediaDetailFragment;
public ReviewPagerAdapter reviewPagerAdapter;
public ReviewController reviewController;
@Inject
ReviewHelper reviewHelper;
@Inject
DeleteHelper deleteHelper;
/**
* Represent fragment for ReviewImage
* Use to call some methods of ReviewImage fragment
*/
private ReviewImageFragment reviewImageFragment;
/**
* Flag to check whether there are any non-hidden categories in the File
*/
private boolean hasNonHiddenCategories = false;
final String SAVED_MEDIA = "saved_media";
private Media media;
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (media != null) {
outState.putParcelable(SAVED_MEDIA, media);
}
}
/**
* Consumers should be simply using this method to use this activity.
*
* @param context
* @param title Page title
*/
public static void startYourself(Context context, String title) {
Intent reviewActivity = new Intent(context, ReviewActivity.class);
reviewActivity.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
reviewActivity.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
context.startActivity(reviewActivity);
}
private CompositeDisposable compositeDisposable = new CompositeDisposable();
public Media getMedia() {
return media;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityReviewBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbarBinding.toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
reviewController = new ReviewController(deleteHelper, this);
reviewPagerAdapter = new ReviewPagerAdapter(getSupportFragmentManager());
binding.viewPagerReview.setAdapter(reviewPagerAdapter);
binding.pagerIndicatorReview.setViewPager(binding.viewPagerReview);
binding.pbReviewImage.setVisibility(View.VISIBLE);
Drawable d[]=binding.skipImage.getCompoundDrawablesRelative();
d[2].setColorFilter(getApplicationContext().getResources().getColor(R.color.button_blue), PorterDuff.Mode.SRC_IN);
if (savedInstanceState != null && savedInstanceState.getParcelable(SAVED_MEDIA) != null) {
updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)); // Use existing media if we have one
setUpMediaDetailOnOrientation();
} else {
runRandomizer(); //Run randomizer whenever everything is ready so that a first random image will be added
}
binding.skipImage.setOnClickListener(view -> {
reviewImageFragment = getInstanceOfReviewImageFragment();
reviewImageFragment.disableButtons();
runRandomizer();
});
binding.reviewImageView.setOnClickListener(view ->setUpMediaDetailFragment());
binding.skipImage.setOnTouchListener((view, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP && event.getRawX() >= (
binding.skipImage.getRight() - binding.skipImage
.getCompoundDrawables()[2].getBounds().width())) {
showSkipImageInfo();
return true;
}
return false;
});
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
@SuppressLint("CheckResult")
public boolean runRandomizer() {
hasNonHiddenCategories = false;
binding.pbReviewImage.setVisibility(View.VISIBLE);
binding.viewPagerReview.setCurrentItem(0);
// Finds non-hidden categories from Media instance
compositeDisposable.add(reviewHelper.getRandomMedia()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::checkWhetherFileIsUsedInWikis));
return true;
}
/**
* Check whether media is used or not in any Wiki Page
*/
@SuppressLint("CheckResult")
private void checkWhetherFileIsUsedInWikis(final Media media) {
compositeDisposable.add(reviewHelper.checkFileUsage(media.getFilename())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
// result false indicates media is not used in any wiki
if (!result) {
// Finds non-hidden categories from Media instance
findNonHiddenCategories(media);
} else {
runRandomizer();
}
}));
}
/**
* Finds non-hidden categories and updates current image
*/
private void findNonHiddenCategories(Media media) {
for(String key : media.getCategoriesHiddenStatus().keySet()) {
Boolean value = media.getCategoriesHiddenStatus().get(key);
// If non-hidden category is found then set hasNonHiddenCategories to true
// so that category review cannot be skipped
if(!value) {
hasNonHiddenCategories = true;
break;
}
}
reviewImageFragment = getInstanceOfReviewImageFragment();
reviewImageFragment.disableButtons();
updateImage(media);
}
@SuppressLint("CheckResult")
private void updateImage(Media media) {
reviewHelper.addViewedImagesToDB(media.getPageId());
this.media = media;
String fileName = media.getFilename();
if (fileName.length() == 0) {
ViewUtil.showShortSnackbar(binding.drawerLayout, R.string.error_review);
return;
}
//If The Media User and Current Session Username is same then Skip the Image
if (media.getUser() != null && media.getUser().equals(AccountUtil.getUserName(getApplicationContext()))) {
runRandomizer();
return;
}
binding.reviewImageView.setImageURI(media.getImageUrl());
reviewController.onImageRefreshed(media); //file name is updated
compositeDisposable.add(reviewHelper.getFirstRevisionOfFile(fileName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(revision -> {
reviewController.firstRevision = revision;
reviewPagerAdapter.updateFileInformation();
@SuppressLint({"StringFormatInvalid", "LocalSuppress"}) String caption = String.format(getString(R.string.review_is_uploaded_by), fileName, revision.getUser());
binding.tvImageCaption.setText(caption);
binding.pbReviewImage.setVisibility(View.GONE);
reviewImageFragment = getInstanceOfReviewImageFragment();
reviewImageFragment.enableButtons();
}));
binding.viewPagerReview.setCurrentItem(0);
}
public void swipeToNext() {
int nextPos = binding.viewPagerReview.getCurrentItem() + 1;
// If currently at category fragment, then check whether the media has any non-hidden category
if (nextPos <= 3) {
binding.viewPagerReview.setCurrentItem(nextPos);
if (nextPos == 2) {
// The media has no non-hidden category. Such media are already flagged by server-side bots, so no need to review manually.
if (!hasNonHiddenCategories) {
swipeToNext();
return;
}
}
} else {
runRandomizer();
}
}
@Override
public void onDestroy() {
super.onDestroy();
compositeDisposable.clear();
binding = null;
}
public void showSkipImageInfo(){
DialogUtil.showAlertDialog(ReviewActivity.this,
getString(R.string.skip_image).toUpperCase(Locale.ROOT),
getString(R.string.skip_image_explanation),
getString(android.R.string.ok),
"",
null,
null);
}
public void showReviewImageInfo() {
DialogUtil.showAlertDialog(ReviewActivity.this,
getString(R.string.title_activity_review),
getString(R.string.review_image_explanation),
getString(android.R.string.ok),
"",
null,
null);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_review_activty, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_image_info:
showReviewImageInfo();
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* this function return the instance of reviewImageFragment
*/
public ReviewImageFragment getInstanceOfReviewImageFragment(){
int currentItemOfReviewPager = binding.viewPagerReview.getCurrentItem();
reviewImageFragment = (ReviewImageFragment) reviewPagerAdapter.instantiateItem(binding.viewPagerReview, currentItemOfReviewPager);
return reviewImageFragment;
}
/**
* set up the media detail fragment when click on the review image
*/
private void setUpMediaDetailFragment() {
if (binding.mediaDetailContainer.getVisibility() == View.GONE && media != null) {
binding.mediaDetailContainer.setVisibility(View.VISIBLE);
binding.reviewActivityContainer.setVisibility(View.INVISIBLE);
FragmentManager fragmentManager = getSupportFragmentManager();
mediaDetailFragment = new MediaDetailFragment();
Bundle bundle = new Bundle();
bundle.putParcelable("media", media);
mediaDetailFragment.setArguments(bundle);
fragmentManager.beginTransaction().add(R.id.mediaDetailContainer, mediaDetailFragment).
addToBackStack("MediaDetail").commit();
}
}
/**
* handle the back pressed event of this activity
* this function call every time when back button is pressed
*/
@Override
public void onBackPressed() {
if (binding.mediaDetailContainer.getVisibility() == View.VISIBLE) {
binding.mediaDetailContainer.setVisibility(View.GONE);
binding.reviewActivityContainer.setVisibility(View.VISIBLE);
}
super.onBackPressed();
}
/**
* set up media detail fragment after orientation change
*/
private void setUpMediaDetailOnOrientation() {
Fragment mediaDetailFragment = getSupportFragmentManager()
.findFragmentById(R.id.mediaDetailContainer);
if (mediaDetailFragment != null) {
binding.mediaDetailContainer.setVisibility(View.VISIBLE);
binding.reviewActivityContainer.setVisibility(View.INVISIBLE);
getSupportFragmentManager().beginTransaction()
.replace(R.id.mediaDetailContainer, mediaDetailFragment).commit();
}
}
}

View file

@ -0,0 +1,336 @@
package fr.free.nrw.commons.review
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.PorterDuff
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.AccountUtil
import fr.free.nrw.commons.databinding.ActivityReviewBinding
import fr.free.nrw.commons.delete.DeleteHelper
import fr.free.nrw.commons.media.MediaDetailFragment
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.utils.DialogUtil
import fr.free.nrw.commons.utils.ViewUtil
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import java.util.Locale
import javax.inject.Inject
class ReviewActivity : BaseActivity() {
private lateinit var binding: ActivityReviewBinding
private var mediaDetailFragment: MediaDetailFragment? = null
lateinit var reviewPagerAdapter: ReviewPagerAdapter
lateinit var reviewController: ReviewController
@Inject
lateinit var reviewHelper: ReviewHelper
@Inject
lateinit var deleteHelper: DeleteHelper
/**
* Represent fragment for ReviewImage
* Use to call some methods of ReviewImage fragment
*/
private var reviewImageFragment: ReviewImageFragment? = null
private var hasNonHiddenCategories = false
var media: Media? = null
private val SAVED_MEDIA = "saved_media"
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
media?.let {
outState.putParcelable(SAVED_MEDIA, it)
}
}
/**
* Consumers should be simply using this method to use this activity.
*
* @param context
* @param title Page title
*/
companion object {
fun startYourself(context: Context, title: String) {
val reviewActivity = Intent(context, ReviewActivity::class.java)
reviewActivity.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
reviewActivity.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
context.startActivity(reviewActivity)
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityReviewBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbarBinding?.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
reviewController = ReviewController(deleteHelper, this)
reviewPagerAdapter = ReviewPagerAdapter(supportFragmentManager)
binding.viewPagerReview.adapter = reviewPagerAdapter
binding.pagerIndicatorReview.setViewPager(binding.viewPagerReview)
binding.pbReviewImage.visibility = View.VISIBLE
binding.skipImage.compoundDrawablesRelative[2]?.setColorFilter(
resources.getColor(R.color.button_blue),
PorterDuff.Mode.SRC_IN
)
if (savedInstanceState?.getParcelable<Media>(SAVED_MEDIA) != null) {
updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)!!)
setUpMediaDetailOnOrientation()
} else {
runRandomizer()
}
binding.skipImage.setOnClickListener {
reviewImageFragment = getInstanceOfReviewImageFragment()
reviewImageFragment?.disableButtons()
runRandomizer()
}
binding.reviewImageView.setOnClickListener {
setUpMediaDetailFragment()
}
binding.skipImage.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP &&
event.rawX >= (binding.skipImage.right - binding.skipImage.compoundDrawables[2].bounds.width())
) {
showSkipImageInfo()
true
} else {
false
}
}
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
@SuppressLint("CheckResult")
fun runRandomizer(): Boolean {
hasNonHiddenCategories = false
binding.pbReviewImage.visibility = View.VISIBLE
binding.viewPagerReview.currentItem = 0
compositeDisposable.add(
reviewHelper.getRandomMedia()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(::checkWhetherFileIsUsedInWikis)
)
return true
}
/**
* Check whether media is used or not in any Wiki Page
*/
@SuppressLint("CheckResult")
private fun checkWhetherFileIsUsedInWikis(media: Media) {
compositeDisposable.add(
reviewHelper.checkFileUsage(media.filename)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->
if (!result) {
findNonHiddenCategories(media)
} else {
runRandomizer()
}
}
)
}
/**
* Finds non-hidden categories and updates current image
*/
private fun findNonHiddenCategories(media: Media) {
this.media = media
// If non-hidden category is found then set hasNonHiddenCategories to true
// so that category review cannot be skipped
hasNonHiddenCategories = media.categoriesHiddenStatus.values.any { !it }
reviewImageFragment = getInstanceOfReviewImageFragment()
reviewImageFragment?.disableButtons()
updateImage(media)
}
@SuppressLint("CheckResult")
private fun updateImage(media: Media) {
reviewHelper.addViewedImagesToDB(media.pageId)
this.media = media
val fileName = media.filename
if (fileName.isNullOrEmpty()) {
ViewUtil.showShortSnackbar(binding.drawerLayout, R.string.error_review)
return
}
//If The Media User and Current Session Username is same then Skip the Image
if (media.user == AccountUtil.getUserName(applicationContext)) {
runRandomizer()
return
}
binding.reviewImageView.setImageURI(media.imageUrl)
reviewController.onImageRefreshed(media) // filename is updated
compositeDisposable.add(
reviewHelper.getFirstRevisionOfFile(fileName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { revision ->
reviewController.firstRevision = revision
reviewPagerAdapter.updateFileInformation()
val caption = getString(
R.string.review_is_uploaded_by,
fileName,
revision.user
)
binding.tvImageCaption.text = caption
binding.pbReviewImage.visibility = View.GONE
reviewImageFragment = getInstanceOfReviewImageFragment()
reviewImageFragment?.enableButtons()
}
)
binding.viewPagerReview.currentItem = 0
}
fun swipeToNext() {
val nextPos = binding.viewPagerReview.currentItem + 1
// If currently at category fragment, then check whether the media has any non-hidden category
if (nextPos <= 3) {
binding.viewPagerReview.currentItem = nextPos
if (nextPos == 2 && !hasNonHiddenCategories)
{
// The media has no non-hidden category. Such media are already flagged by server-side bots, so no need to review manually.
swipeToNext()
}
} else {
runRandomizer()
}
}
public override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
}
fun showSkipImageInfo() {
DialogUtil.showAlertDialog(
this,
getString(R.string.skip_image).uppercase(Locale.ROOT),
getString(R.string.skip_image_explanation),
getString(android.R.string.ok),
null,
null,
null
)
}
fun showReviewImageInfo() {
DialogUtil.showAlertDialog(
this,
getString(R.string.title_activity_review),
getString(R.string.review_image_explanation),
getString(android.R.string.ok),
null,
null,
null
)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_review_activty, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_image_info -> {
showReviewImageInfo()
true
}
else -> super.onOptionsItemSelected(item)
}
}
/**
* this function return the instance of reviewImageFragment
*/
private fun getInstanceOfReviewImageFragment(): ReviewImageFragment? {
val currentItemOfReviewPager = binding.viewPagerReview.currentItem
return reviewPagerAdapter.instantiateItem(
binding.viewPagerReview,
currentItemOfReviewPager
) as? ReviewImageFragment
}
/**
* set up the media detail fragment when click on the review image
*/
private fun setUpMediaDetailFragment() {
if (binding.mediaDetailContainer.visibility == View.GONE && media != null) {
binding.mediaDetailContainer.visibility = View.VISIBLE
binding.reviewActivityContainer.visibility = View.INVISIBLE
val fragmentManager = supportFragmentManager
mediaDetailFragment = MediaDetailFragment().apply {
arguments = Bundle().apply {
putParcelable("media", media)
}
}
fragmentManager.beginTransaction()
.add(R.id.mediaDetailContainer, mediaDetailFragment!!)
.addToBackStack("MediaDetail")
.commit()
}
}
/**
* handle the back pressed event of this activity
* this function call every time when back button is pressed
*/
@Deprecated("This method has been deprecated in favor of using the" +
"{@link OnBackPressedDispatcher} via {@link #getOnBackPressedDispatcher()}." +
"The OnBackPressedDispatcher controls how back button events are dispatched" +
"to one or more {@link OnBackPressedCallback} objects.")
override fun onBackPressed() {
if (binding.mediaDetailContainer.visibility == View.VISIBLE) {
binding.mediaDetailContainer.visibility = View.GONE
binding.reviewActivityContainer.visibility = View.VISIBLE
}
super.onBackPressed()
}
/**
* set up media detail fragment after orientation change
*/
private fun setUpMediaDetailOnOrientation() {
val fragment = supportFragmentManager.findFragmentById(R.id.mediaDetailContainer)
fragment?.let {
binding.mediaDetailContainer.visibility = View.VISIBLE
binding.reviewActivityContainer.visibility = View.INVISIBLE
supportFragmentManager.beginTransaction()
.replace(R.id.mediaDetailContainer, it)
.commit()
}
}
}

View file

@ -1,220 +0,0 @@
package fr.free.nrw.commons.review;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.actions.ThanksClient;
import fr.free.nrw.commons.delete.DeleteHelper;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
@Singleton
public class ReviewController {
private static final int NOTIFICATION_SEND_THANK = 0x102;
private static final int NOTIFICATION_CHECK_CATEGORY = 0x101;
protected static ArrayList<String> categories;
@Inject
ThanksClient thanksClient;
@Inject
SessionManager sessionManager;
private final DeleteHelper deleteHelper;
@Nullable
MwQueryPage.Revision firstRevision; // TODO: maybe we can expand this class to include fileName
@Inject
@Named("commons-page-edit")
PageEditClient pageEditClient;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;
private Media media;
ReviewController(DeleteHelper deleteHelper, Context context) {
this.deleteHelper = deleteHelper;
CommonsApplication.createNotificationChannel(context.getApplicationContext());
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationBuilder = new NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL);
}
void onImageRefreshed(Media media) {
this.media = media;
}
public Media getMedia() {
return media;
}
public enum DeleteReason {
SPAM,
COPYRIGHT_VIOLATION
}
void reportSpam(@NonNull Activity activity, ReviewCallback reviewCallback) {
Timber.d("Report spam for %s", media.getFilename());
deleteHelper.askReasonAndExecute(media,
activity,
activity.getResources().getString(R.string.review_spam_report_question),
DeleteReason.SPAM,
reviewCallback);
}
void reportPossibleCopyRightViolation(@NonNull Activity activity, ReviewCallback reviewCallback) {
Timber.d("Report spam for %s", media.getFilename());
deleteHelper.askReasonAndExecute(media,
activity,
activity.getResources().getString(R.string.review_c_violation_report_question),
DeleteReason.COPYRIGHT_VIOLATION,
reviewCallback);
}
@SuppressLint("CheckResult")
void reportWrongCategory(@NonNull Activity activity, ReviewCallback reviewCallback) {
Context context = activity.getApplicationContext();
ApplicationlessInjection
.getInstance(context)
.getCommonsApplicationComponent()
.inject(this);
ViewUtil.showShortToast(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()));
publishProgress(context, 0);
String summary = context.getString(R.string.check_category_edit_summary);
Observable.defer((Callable<ObservableSource<Boolean>>) () ->
pageEditClient.appendEdit(media.getFilename(), "\n{{subst:chc}}\n", summary))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((result) -> {
publishProgress(context, 2);
String message;
String title;
if (result) {
title = context.getString(R.string.check_category_success_title);
message = context.getString(R.string.check_category_success_message, media.getDisplayTitle());
reviewCallback.onSuccess();
} else {
title = context.getString(R.string.check_category_failure_title);
message = context.getString(R.string.check_category_failure_message, media.getDisplayTitle());
reviewCallback.onFailure();
}
showNotification(title, message);
}, Timber::e);
}
private void publishProgress(@NonNull Context context, int i) {
int[] messages = new int[]{R.string.getting_edit_token, R.string.check_category_adding_template};
String message = "";
if (0 < i && i < messages.length) {
message = context.getString(messages[i]);
}
notificationBuilder.setContentTitle(context.getString(R.string.check_category_notification_title, media.getDisplayTitle()))
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(message))
.setSmallIcon(R.drawable.ic_launcher)
.setProgress(messages.length, i, false)
.setOngoing(true);
notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build());
}
@SuppressLint({"CheckResult", "StringFormatInvalid"})
void sendThanks(@NonNull Activity activity) {
Context context = activity.getApplicationContext();
ApplicationlessInjection
.getInstance(context)
.getCommonsApplicationComponent()
.inject(this);
ViewUtil.showShortToast(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()));
if (firstRevision == null) {
return;
}
Observable.defer((Callable<ObservableSource<Boolean>>) () -> thanksClient.thank(firstRevision.getRevisionId()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
displayThanksToast(context, result);
}, throwable -> {
if (throwable instanceof InvalidLoginTokenException) {
final String username = sessionManager.getUserName();
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
activity,
activity.getString(R.string.invalid_login_message),
username
);
CommonsApplication.getInstance().clearApplicationData(
activity, logoutListener);
} else {
Timber.e(throwable);
}
});
}
@SuppressLint("StringFormatInvalid")
private void displayThanksToast(final Context context, final boolean result){
final String message;
final String title;
if (result) {
title = context.getString(R.string.send_thank_success_title);
message = context.getString(R.string.send_thank_success_message, media.getDisplayTitle());
} else {
title = context.getString(R.string.send_thank_failure_title);
message = context.getString(R.string.send_thank_failure_message, media.getDisplayTitle());
}
ViewUtil.showShortToast(context,message);
}
private void showNotification(String title, String message) {
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
.setContentTitle(title)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(message))
.setSmallIcon(R.drawable.ic_launcher)
.setProgress(0, 0, false)
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_HIGH);
notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build());
}
public interface ReviewCallback {
void onSuccess();
void onFailure();
void onTokenException(Exception e);
void disableButtons();
void enableButtons();
}
}

View file

@ -0,0 +1,231 @@
package fr.free.nrw.commons.review
import android.annotation.SuppressLint
import android.app.Activity
import android.app.NotificationManager
import android.content.Context
import androidx.core.app.NotificationCompat
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage
import java.util.ArrayList
import java.util.concurrent.Callable
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.actions.ThanksClient
import fr.free.nrw.commons.delete.DeleteHelper
import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.utils.ViewUtil
import io.reactivex.Observable
import io.reactivex.ObservableSource
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
@Singleton
class ReviewController @Inject constructor(
private val deleteHelper: DeleteHelper,
context: Context
) {
companion object {
private const val NOTIFICATION_SEND_THANK = 0x102
private const val NOTIFICATION_CHECK_CATEGORY = 0x101
protected var categories: ArrayList<String> = ArrayList()
}
@Inject
lateinit var thanksClient: ThanksClient
@Inject
lateinit var sessionManager: SessionManager
@Inject
@field: Named("commons-page-edit")
lateinit var pageEditClient: PageEditClient
var firstRevision: MwQueryPage.Revision? = null // TODO: maybe we can expand this class to include fileName
private val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val notificationBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)
var media: Media? = null
init {
CommonsApplication.createNotificationChannel(context.applicationContext)
}
fun onImageRefreshed(media: Media) {
this.media = media
}
enum class DeleteReason {
SPAM,
COPYRIGHT_VIOLATION
}
fun reportSpam(activity: Activity, reviewCallback: ReviewCallback) {
Timber.d("Report spam for %s", media?.filename)
deleteHelper.askReasonAndExecute(
media,
activity,
activity.resources.getString(R.string.review_spam_report_question),
DeleteReason.SPAM,
reviewCallback
)
}
fun reportPossibleCopyRightViolation(activity: Activity, reviewCallback: ReviewCallback) {
Timber.d("Report copyright violation for %s", media?.filename)
deleteHelper.askReasonAndExecute(
media,
activity,
activity.resources.getString(R.string.review_c_violation_report_question),
DeleteReason.COPYRIGHT_VIOLATION,
reviewCallback
)
}
@SuppressLint("CheckResult")
fun reportWrongCategory(activity: Activity, reviewCallback: ReviewCallback) {
val context = activity.applicationContext
ApplicationlessInjection
.getInstance(context)
.commonsApplicationComponent
.inject(this)
ViewUtil.showShortToast(
context,
context.getString(R.string.check_category_toast, media?.displayTitle)
)
publishProgress(context, 0)
val summary = context.getString(R.string.check_category_edit_summary)
Observable.defer {
pageEditClient.appendEdit(media?.filename ?: "", "\n{{subst:chc}}\n", summary)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result ->
publishProgress(context, 2)
val (title, message) = if (result) {
reviewCallback.onSuccess()
context.getString(R.string.check_category_success_title) to
context.getString(R.string.check_category_success_message, media?.displayTitle)
} else {
reviewCallback.onFailure()
context.getString(R.string.check_category_failure_title) to
context.getString(R.string.check_category_failure_message, media?.displayTitle)
}
showNotification(title, message)
}, Timber::e)
}
private fun publishProgress(context: Context, progress: Int) {
val messages = arrayOf(
R.string.getting_edit_token,
R.string.check_category_adding_template
)
val message = if (progress in 1 until messages.size) {
context.getString(messages[progress])
} else ""
notificationBuilder.setContentTitle(
context.getString(
R.string.check_category_notification_title,
media?.displayTitle
)
)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
.setSmallIcon(R.drawable.ic_launcher)
.setProgress(messages.size, progress, false)
.setOngoing(true)
notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build())
}
@SuppressLint("CheckResult")
fun sendThanks(activity: Activity) {
val context = activity.applicationContext
ApplicationlessInjection
.getInstance(context)
.commonsApplicationComponent
.inject(this)
ViewUtil.showShortToast(
context,
context.getString(R.string.send_thank_toast, media?.displayTitle)
)
if (firstRevision == null) return
Observable.defer {
thanksClient.thank(firstRevision!!.revisionId)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result ->
displayThanksToast(context, result)
}, { throwable ->
if (throwable is InvalidLoginTokenException) {
val username = sessionManager.userName
val logoutListener = CommonsApplication.BaseLogoutListener(
activity,
activity.getString(R.string.invalid_login_message),
username
)
CommonsApplication.instance.clearApplicationData(activity, logoutListener)
} else {
Timber.e(throwable)
}
})
}
@SuppressLint("StringFormatInvalid")
private fun displayThanksToast(context: Context, result: Boolean) {
val (title, message) = if (result) {
context.getString(R.string.send_thank_success_title) to
context.getString(R.string.send_thank_success_message, media?.displayTitle)
} else {
context.getString(R.string.send_thank_failure_title) to
context.getString(R.string.send_thank_failure_message, media?.displayTitle)
}
ViewUtil.showShortToast(context, message)
}
private fun showNotification(title: String, message: String) {
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
.setContentTitle(title)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
.setSmallIcon(R.drawable.ic_launcher)
.setProgress(0, 0, false)
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_HIGH)
notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build())
}
interface ReviewCallback {
fun onSuccess()
fun onFailure()
fun onTokenException(e: Exception)
fun disableButtons()
fun enableButtons()
}
}

View file

@ -1,15 +1,15 @@
package fr.free.nrw.commons.review;
package fr.free.nrw.commons.review
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
/**
* Dao interface for reviewed images database
*/
@Dao
public interface ReviewDao {
interface ReviewDao {
/**
* Inserts reviewed/skipped image identifier into the database
@ -17,7 +17,7 @@ public interface ReviewDao {
* @param reviewEntity
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
void insert(ReviewEntity reviewEntity);
fun insert(reviewEntity: ReviewEntity)
/**
* Checks if the image has already been reviewed/skipped by the user
@ -26,7 +26,6 @@ public interface ReviewDao {
* @param imageId
* @return
*/
@Query( "SELECT EXISTS (SELECT * from `reviewed-images` where imageId = (:imageId))")
Boolean isReviewedAlready(String imageId);
}
@Query("SELECT EXISTS (SELECT * from `reviewed-images` where imageId = (:imageId))")
fun isReviewedAlready(imageId: String): Boolean
}

View file

@ -1,19 +0,0 @@
package fr.free.nrw.commons.review;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
/**
* Entity to store reviewed/skipped images identifier
*/
@Entity(tableName = "reviewed-images")
public class ReviewEntity {
@PrimaryKey
@NonNull
String imageId;
public ReviewEntity(String imageId) {
this.imageId = imageId;
}
}

View file

@ -0,0 +1,13 @@
package fr.free.nrw.commons.review
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* Entity to store reviewed/skipped images identifier
*/
@Entity(tableName = "reviewed-images")
data class ReviewEntity(
@PrimaryKey
val imageId: String
)

View file

@ -77,7 +77,7 @@ class ReviewHelper
* @param image
* @return
*/
fun getReviewStatus(image: String?): Boolean = dao?.isReviewedAlready(image) ?: false
fun getReviewStatus(image: String?): Boolean = image?.let { dao?.isReviewedAlready(it) } ?: false
/**
* Gets the first revision of the file from filename
@ -132,7 +132,7 @@ class ReviewHelper
*/
fun addViewedImagesToDB(imageId: String?) {
Completable
.fromAction { dao!!.insert(ReviewEntity(imageId)) }
.fromAction { imageId?.let { ReviewEntity(it) }?.let { dao!!.insert(it) } }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(

View file

@ -1,262 +0,0 @@
package fr.free.nrw.commons.review;
import android.graphics.Color;
import android.os.Bundle;
import android.text.Html;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
import fr.free.nrw.commons.databinding.FragmentReviewImageBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
public class ReviewImageFragment extends CommonsDaggerSupportFragment {
static final int CATEGORY = 2;
private static final int SPAM = 0;
private static final int COPYRIGHT = 1;
private static final int THANKS = 3;
private int position;
private FragmentReviewImageBinding binding;
@Inject
SessionManager sessionManager;
// Constant variable used to store user's key name for onSaveInstanceState method
private final String SAVED_USER = "saved_user";
// Variable that stores the value of user
private String user;
public void update(final int position) {
this.position = position;
}
private String updateCategoriesQuestion() {
final Media media = getReviewActivity().getMedia();
if (media != null && media.getCategoriesHiddenStatus() != null && isAdded()) {
// Filter category name attribute from all categories
final List<String> categories = new ArrayList<>();
for(final String key : media.getCategoriesHiddenStatus().keySet()) {
String value = String.valueOf(key);
// Each category returned has a format like "Category:<some-category-name>"
// so remove the prefix "Category:"
final int index = key.indexOf("Category:");
if(index == 0) {
value = key.substring(9);
}
categories.add(value);
}
String catString = TextUtils.join(", ", categories);
if (catString != null && !catString.equals("") && binding.tvReviewQuestionContext != null) {
catString = "<b>" + catString + "</b>";
final String stringToConvertHtml = String.format(getResources().getString(R.string.review_category_explanation), catString);
return Html.fromHtml(stringToConvertHtml).toString();
}
}
return getResources().getString(R.string.review_no_category);
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
position = getArguments().getInt("position");
binding = FragmentReviewImageBinding.inflate(inflater, container, false);
final String question;
String explanation=null;
String yesButtonText;
final String noButtonText;
binding.buttonYes.setOnClickListener(view -> onYesButtonClicked());
switch (position) {
case SPAM:
question = getString(R.string.review_spam);
explanation = getString(R.string.review_spam_explanation);
yesButtonText = getString(R.string.yes);
noButtonText = getString(R.string.no);
binding.buttonNo.setOnClickListener(view -> getReviewActivity()
.reviewController.reportSpam(requireActivity(), getReviewCallback()));
break;
case COPYRIGHT:
enableButtons();
question = getString(R.string.review_copyright);
explanation = getString(R.string.review_copyright_explanation);
yesButtonText = getString(R.string.yes);
noButtonText = getString(R.string.no);
binding.buttonNo.setOnClickListener(view -> getReviewActivity()
.reviewController
.reportPossibleCopyRightViolation(requireActivity(), getReviewCallback()));
break;
case CATEGORY:
enableButtons();
question = getString(R.string.review_category);
explanation = updateCategoriesQuestion();
yesButtonText = getString(R.string.yes);
noButtonText = getString(R.string.no);
binding.buttonNo.setOnClickListener(view -> {
getReviewActivity()
.reviewController
.reportWrongCategory(requireActivity(), getReviewCallback());
getReviewActivity().swipeToNext();
});
break;
case THANKS:
enableButtons();
question = getString(R.string.review_thanks);
if (getReviewActivity().reviewController.firstRevision != null) {
user = getReviewActivity().reviewController.firstRevision.getUser();
} else {
if(savedInstanceState != null) {
user = savedInstanceState.getString(SAVED_USER);
}
}
//if the user is null because of whatsoever reason, review will not be sent anyways
if (!TextUtils.isEmpty(user)) {
explanation = getString(R.string.review_thanks_explanation, user);
}
// Note that the yes and no buttons are swapped in this section
yesButtonText = getString(R.string.review_thanks_yes_button_text);
noButtonText = getString(R.string.review_thanks_no_button_text);
binding.buttonYes.setTextColor(Color.parseColor("#116aaa"));
binding.buttonNo.setTextColor(Color.parseColor("#228b22"));
binding.buttonNo.setOnClickListener(view -> {
getReviewActivity().reviewController.sendThanks(getReviewActivity());
getReviewActivity().swipeToNext();
});
break;
default:
enableButtons();
question = "How did we get here?";
explanation = "No idea.";
yesButtonText = "yes";
noButtonText = "no";
}
binding.tvReviewQuestion.setText(question);
binding.tvReviewQuestionContext.setText(explanation);
binding.buttonYes.setText(yesButtonText);
binding.buttonNo.setText(noButtonText);
return binding.getRoot();
}
/**
* This method will be called when configuration changes happen
*
* @param outState
*/
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
//Save user name when configuration changes happen
outState.putString(SAVED_USER, user);
}
private ReviewController.ReviewCallback getReviewCallback() {
return new ReviewController
.ReviewCallback() {
@Override
public void onSuccess() {
getReviewActivity().runRandomizer();
}
@Override
public void onFailure() {
//do nothing
}
@Override
public void onTokenException(final Exception e) {
if (e instanceof InvalidLoginTokenException){
final String username = sessionManager.getUserName();
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
getActivity(),
requireActivity().getString(R.string.invalid_login_message),
username
);
CommonsApplication.getInstance().clearApplicationData(
requireActivity(), logoutListener);
}
}
/**
* This function is called when an image is being loaded
* to disable the review buttons
*/
@Override
public void disableButtons() {
ReviewImageFragment.this.disableButtons();
}
/**
* This function is called when an image has
* been loaded to enable the review buttons.
*/
@Override
public void enableButtons() {
ReviewImageFragment.this.enableButtons();
}
};
}
/**
* This function is called when an image has
* been loaded to enable the review buttons.
*/
public void enableButtons() {
binding.buttonYes.setEnabled(true);
binding.buttonYes.setAlpha(1);
binding.buttonNo.setEnabled(true);
binding.buttonNo.setAlpha(1);
}
/**
* This function is called when an image is being loaded
* to disable the review buttons
*/
public void disableButtons() {
binding.buttonYes.setEnabled(false);
binding.buttonYes.setAlpha(0.5f);
binding.buttonNo.setEnabled(false);
binding.buttonNo.setAlpha(0.5f);
}
void onYesButtonClicked() {
getReviewActivity().swipeToNext();
}
private ReviewActivity getReviewActivity() {
return (ReviewActivity) requireActivity();
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,251 @@
package fr.free.nrw.commons.review
import android.graphics.Color
import android.os.Bundle
import android.text.Html
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.databinding.FragmentReviewImageBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import java.util.ArrayList
import javax.inject.Inject
class ReviewImageFragment : CommonsDaggerSupportFragment() {
companion object {
const val CATEGORY = 2
private const val SPAM = 0
private const val COPYRIGHT = 1
private const val THANKS = 3
}
private var position: Int = 0
private var binding: FragmentReviewImageBinding? = null
@Inject
lateinit var sessionManager: SessionManager
// Constant variable used to store user's key name for onSaveInstanceState method
private val SAVED_USER = "saved_user"
// Variable that stores the value of user
private var user: String? = null
fun update(position: Int) {
this.position = position
}
private fun updateCategoriesQuestion(): String {
val media = reviewActivity.media
if (media?.categoriesHiddenStatus != null && isAdded) {
// Filter category name attribute from all categories
val categories = media.categoriesHiddenStatus.keys.map { key ->
var value = key
// Each category returned has a format like "Category:<some-category-name>"
// so remove the prefix "Category:"
if (key.startsWith("Category:")) {
value = key.substring(9)
}
value
}
val catString = categories.joinToString(", ")
if (catString.isNotEmpty() && binding?.tvReviewQuestionContext != null) {
val formattedCatString = "<b>$catString</b>"
val stringToConvertHtml = getString(
R.string.review_category_explanation,
formattedCatString
)
return Html.fromHtml(stringToConvertHtml).toString()
}
}
return getString(R.string.review_no_category)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
position = requireArguments().getInt("position")
binding = FragmentReviewImageBinding.inflate(inflater, container, false)
val question: String
var explanation: String? = null
val yesButtonText: String
val noButtonText: String
binding?.buttonYes?.setOnClickListener { onYesButtonClicked() }
when (position) {
SPAM -> {
question = getString(R.string.review_spam)
explanation = getString(R.string.review_spam_explanation)
yesButtonText = getString(R.string.yes)
noButtonText = getString(R.string.no)
binding?.buttonNo?.setOnClickListener {
reviewActivity.reviewController.reportSpam(requireActivity(), reviewCallback)
}
}
COPYRIGHT -> {
enableButtons()
question = getString(R.string.review_copyright)
explanation = getString(R.string.review_copyright_explanation)
yesButtonText = getString(R.string.yes)
noButtonText = getString(R.string.no)
binding?.buttonNo?.setOnClickListener {
reviewActivity.reviewController.reportPossibleCopyRightViolation(
requireActivity(),
reviewCallback
)
}
}
CATEGORY -> {
enableButtons()
question = getString(R.string.review_category)
explanation = updateCategoriesQuestion()
yesButtonText = getString(R.string.yes)
noButtonText = getString(R.string.no)
binding?.buttonNo?.setOnClickListener {
reviewActivity.reviewController.reportWrongCategory(
requireActivity(),
reviewCallback
)
reviewActivity.swipeToNext()
}
}
THANKS -> {
enableButtons()
question = getString(R.string.review_thanks)
user = reviewActivity.reviewController.firstRevision?.user
?: savedInstanceState?.getString(SAVED_USER)
//if the user is null because of whatsoever reason, review will not be sent anyways
if (!user.isNullOrEmpty()) {
explanation = getString(R.string.review_thanks_explanation, user)
}
// Note that the yes and no buttons are swapped in this section
yesButtonText = getString(R.string.review_thanks_yes_button_text)
noButtonText = getString(R.string.review_thanks_no_button_text)
binding?.buttonYes?.setTextColor(Color.parseColor("#116aaa"))
binding?.buttonNo?.setTextColor(Color.parseColor("#228b22"))
binding?.buttonNo?.setOnClickListener {
reviewActivity.reviewController.sendThanks(requireActivity())
reviewActivity.swipeToNext()
}
}
else -> {
enableButtons()
question = "How did we get here?"
explanation = "No idea."
yesButtonText = "yes"
noButtonText = "no"
}
}
binding?.apply {
tvReviewQuestion.text = question
tvReviewQuestionContext.text = explanation
buttonYes.text = yesButtonText
buttonNo.text = noButtonText
}
return binding?.root
}
/**
* This method will be called when configuration changes happen
*
* @param outState
*/
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
//Save user name when configuration changes happen
outState.putString(SAVED_USER, user)
}
private val reviewCallback: ReviewController.ReviewCallback
get() = object : ReviewController.ReviewCallback {
override fun onSuccess() {
reviewActivity.runRandomizer()
}
override fun onFailure() {
//do nothing
}
override fun onTokenException(e: Exception) {
if (e is InvalidLoginTokenException) {
val username = sessionManager.userName
val logoutListener = activity?.let {
CommonsApplication.BaseLogoutListener(
it,
getString(R.string.invalid_login_message),
username
)
}
if (logoutListener != null) {
CommonsApplication.instance.clearApplicationData(
requireActivity(), logoutListener
)
}
}
}
override fun disableButtons() {
this@ReviewImageFragment.disableButtons()
}
override fun enableButtons() {
this@ReviewImageFragment.enableButtons()
}
}
/**
* This function is called when an image has
* been loaded to enable the review buttons.
*/
fun enableButtons() {
binding?.apply {
buttonYes.isEnabled = true
buttonYes.alpha = 1f
buttonNo.isEnabled = true
buttonNo.alpha = 1f
}
}
/**
* This function is called when an image is being loaded
* to disable the review buttons
*/
fun disableButtons() {
binding?.apply {
buttonYes.isEnabled = false
buttonYes.alpha = 0.5f
buttonNo.isEnabled = false
buttonNo.alpha = 0.5f
}
}
fun onYesButtonClicked() {
reviewActivity.swipeToNext()
}
private val reviewActivity: ReviewActivity
get() = requireActivity() as ReviewActivity
override fun onDestroy() {
super.onDestroy()
binding = null
}
}

View file

@ -1,53 +0,0 @@
package fr.free.nrw.commons.review;
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
public class ReviewPagerAdapter extends FragmentStatePagerAdapter {
private ReviewImageFragment[] reviewImageFragments;
/**
* this function return the instance of ReviewviewPage current item
*/
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
return super.instantiateItem(container, position);
}
ReviewPagerAdapter(FragmentManager fm) {
super(fm);
reviewImageFragments = new ReviewImageFragment[]{
new ReviewImageFragment(),
new ReviewImageFragment(),
new ReviewImageFragment(),
new ReviewImageFragment()
};
}
@Override
public int getCount() {
return reviewImageFragments.length;
}
void updateFileInformation() {
for (int i = 0; i < getCount(); i++) {
ReviewImageFragment fragment = reviewImageFragments[i];
fragment.update(i);
}
}
@Override
public Fragment getItem(int position) {
Bundle bundle = new Bundle();
bundle.putInt("position", position);
reviewImageFragments[position].setArguments(bundle);
return reviewImageFragments[position];
}
}

View file

@ -0,0 +1,37 @@
package fr.free.nrw.commons.review
import android.os.Bundle
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
class ReviewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
private val reviewImageFragments: Array<ReviewImageFragment> = arrayOf(
ReviewImageFragment(),
ReviewImageFragment(),
ReviewImageFragment(),
ReviewImageFragment()
)
override fun getCount(): Int {
return reviewImageFragments.size
}
fun updateFileInformation() {
for (i in 0 until count) {
val fragment = reviewImageFragments[i]
fragment.update(i)
}
}
override fun getItem(position: Int): Fragment {
val bundle = Bundle().apply {
putInt("position", position)
}
reviewImageFragments[position].arguments = bundle
return reviewImageFragments[position]
}
}

View file

@ -1,30 +0,0 @@
package fr.free.nrw.commons.review;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.viewpager.widget.ViewPager;
public class ReviewViewPager extends ViewPager {
public ReviewViewPager(Context context) {
super(context);
}
public ReviewViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// Never allow swiping to switch between pages
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Never allow swiping to switch between pages
return false;
}
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.review
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
class ReviewViewPager @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : ViewPager(context, attrs) {
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
// Never allow swiping to switch between pages
return false
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
// Never allow swiping to switch between pages
return false
}
}