From a8387f01c9e7d54b4f6cd7b7da342c64f38de352 Mon Sep 17 00:00:00 2001 From: Neel Doshi <60827173+neeldoshii@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:45:47 +0530 Subject: [PATCH] Bug Fixs & Enhancement of Achievement Screen (#5666) * Rename AchievementFragment from `.java` to `.kt` * Migrated AchievementFragment to kotlin * Revamped Achievement Screen * fixed AchievementFragment Unit Test * fixed Level on MoreBottomSheetFragment * Implemented Badge and Minor Code Refactor * Fixed the badge issue & made the badge clickable * Removed Redundant XML Code & Converted badges to green color and added values inside it * Fixed : showSnackBarWithRetry Test * Fixed : Theme issues on Light Mode --------- Co-authored-by: Nicolas Raoul --- app/build.gradle | 2 +- .../commons/navtab/MoreBottomSheetFragment.kt | 14 +- .../nrw/commons/profile/ProfileActivity.java | 1 - .../achievements/AchievementsFragment.java | 492 --------- .../achievements/AchievementsFragment.kt | 566 ++++++++++ .../main/res/layout/fragment_achievements.xml | 990 +++++++----------- app/src/main/res/values/strings.xml | 4 +- app/src/main/res/values/styles.xml | 5 +- .../AchievementsFragmentUnitTests.kt | 2 +- 9 files changed, 944 insertions(+), 1132 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt diff --git a/app/build.gradle b/app/build.gradle index 468255d38c..b83f2b01ba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,7 +47,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' - implementation "com.google.android.material:material:1.9.0" + implementation "com.google.android.material:material:1.12.0" implementation 'com.karumi:dexter:5.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt index 857e18ec31..cbdf5f0875 100644 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt @@ -111,10 +111,18 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() { private fun setUserName() { val store = BasicKvStore(requireContext(), getUserName()) val level = store.getString("userAchievementsLevel", "0") - binding?.moreProfile?.text = if (level == "0") { - "${getUserName()} (${getString(R.string.see_your_achievements)})" + if (level == "0"){ + binding?.moreProfile?.text = getString( + R.string.profileLevel, + getUserName(), + getString(R.string.see_your_achievements) // Second argument + ) } else { - "${getUserName()} (${getString(R.string.level)} $level)" + binding?.moreProfile?.text = getString( + R.string.profileLevel, + getUserName(), + level + ) } } diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java index 60a0f47a1a..390768416f 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java @@ -16,7 +16,6 @@ import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; -import com.google.android.material.tabs.TabLayout; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.ViewPagerAdapter; diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java deleted file mode 100644 index ef6a323b2e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java +++ /dev/null @@ -1,492 +0,0 @@ -package fr.free.nrw.commons.profile.achievements; - -import android.accounts.Account; -import android.content.Context; -import android.net.Uri; -import android.os.Bundle; -import android.util.DisplayMetrics; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.databinding.FragmentAchievementsBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.kvstore.BasicKvStore; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.ViewUtil; -import fr.free.nrw.commons.profile.ProfileActivity; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Locale; -import java.util.Objects; -import javax.inject.Inject; -import org.apache.commons.lang3.StringUtils; -import timber.log.Timber; - -/** - * fragment for sharing feedback on uploaded activity - */ -public class AchievementsFragment extends CommonsDaggerSupportFragment { - - private static final double BADGE_IMAGE_WIDTH_RATIO = 0.4; - private static final double BADGE_IMAGE_HEIGHT_RATIO = 0.3; - - /** - * Help link URLs - */ - private static final String IMAGES_UPLOADED_URL = "https://commons.wikimedia.org/wiki/Commons:Project_scope"; - private static final String IMAGES_REVERT_URL = "https://commons.wikimedia.org/wiki/Commons:Deletion_policy#Reasons_for_deletion"; - private static final String IMAGES_USED_URL = "https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images"; - private static final String IMAGES_NEARBY_PLACES_URL = "https://www.wikidata.org/wiki/Property:P18"; - private static final String IMAGES_FEATURED_URL = "https://commons.wikimedia.org/wiki/Commons:Featured_pictures"; - private static final String QUALITY_IMAGE_URL = "https://commons.wikimedia.org/wiki/Commons:Quality_images"; - private static final String THANKS_URL = "https://www.mediawiki.org/wiki/Extension:Thanks"; - - private LevelController.LevelInfo levelInfo; - - @Inject - SessionManager sessionManager; - - @Inject - OkHttpJsonApiClient okHttpJsonApiClient; - - private FragmentAchievementsBinding binding; - - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - // To keep track of the number of wiki edits made by a user - private int numberOfEdits = 0; - - private String userName; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getArguments() != null) { - userName = getArguments().getString(ProfileActivity.KEY_USERNAME); - } - } - - /** - * This method helps in the creation Achievement screen and - * dynamically set the size of imageView - * - * @param savedInstanceState Data bundle - */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentAchievementsBinding.inflate(inflater, container, false); - View rootView = binding.getRoot(); - - binding.achievementInfo.setOnClickListener(view -> showInfoDialog()); - binding.imagesUploadInfo.setOnClickListener(view -> showUploadInfo()); - binding.imagesRevertedInfo.setOnClickListener(view -> showRevertedInfo()); - binding.imagesUsedByWikiInfo.setOnClickListener(view -> showUsedByWikiInfo()); - binding.imagesNearbyInfo.setOnClickListener(view -> showImagesViaNearbyInfo()); - binding.imagesFeaturedInfo.setOnClickListener(view -> showFeaturedImagesInfo()); - binding.thanksReceivedInfo.setOnClickListener(view -> showThanksReceivedInfo()); - binding.qualityImagesInfo.setOnClickListener(view -> showQualityImagesInfo()); - - // DisplayMetrics used to fetch the size of the screen - DisplayMetrics displayMetrics = new DisplayMetrics(); - getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - int height = displayMetrics.heightPixels; - int width = displayMetrics.widthPixels; - - // Used for the setting the size of imageView at runtime - ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) - binding.achievementBadgeImage.getLayoutParams(); - params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO); - params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO); - binding.achievementBadgeImage.requestLayout(); - binding.progressBar.setVisibility(View.VISIBLE); - - setHasOptionsMenu(true); - - // Set the initial value of WikiData edits to 0 - binding.wikidataEdits.setText("0"); - if(sessionManager.getUserName() == null || sessionManager.getUserName().equals(userName)){ - binding.tvAchievementsOfUser.setVisibility(View.GONE); - }else{ - binding.tvAchievementsOfUser.setVisibility(View.VISIBLE); - binding.tvAchievementsOfUser.setText(getString(R.string.achievements_of_user,userName)); - } - - // Achievements currently unimplemented in Beta flavor. Skip all API calls. - if(ConfigUtils.isBetaFlavour()) { - binding.progressBar.setVisibility(View.GONE); - binding.imagesUsedByWikiText.setText(R.string.no_image); - binding.imagesRevertedText.setText(R.string.no_image_reverted); - binding.imagesUploadTextParam.setText(R.string.no_image_uploaded); - binding.wikidataEdits.setText("0"); - binding.imageFeatured.setText("0"); - binding.qualityImages.setText("0"); - binding.achievementLevel.setText("0"); - setMenuVisibility(true); - return rootView; - } - setWikidataEditCount(); - setAchievements(); - return rootView; - } - - @Override - public void onDestroyView() { - binding = null; - super.onDestroyView(); - } - - @Override - public void setMenuVisibility(boolean visible) { - super.setMenuVisibility(visible); - - // Whenever this fragment is revealed in a menu, - // notify Beta users the page data is unavailable - if(ConfigUtils.isBetaFlavour() && visible) { - Context ctx = null; - if(getContext() != null) { - ctx = getContext(); - } else if(getView() != null && getView().getContext() != null) { - ctx = getView().getContext(); - } - if(ctx != null) { - Toast.makeText(ctx, - R.string.achievements_unavailable_beta, - Toast.LENGTH_LONG).show(); - } - } - } - - /** - * To invoke the AlertDialog on clicking info button - */ - protected void showInfoDialog(){ - launchAlert( - getResources().getString(R.string.Achievements), - getResources().getString(R.string.achievements_info_message)); - } - - /** - * To call the API to get results in form Single - * which then calls parseJson when results are fetched - */ - private void setAchievements() { - binding.progressBar.setVisibility(View.VISIBLE); - if (checkAccount()) { - try{ - - compositeDisposable.add(okHttpJsonApiClient - .getAchievements(Objects.requireNonNull(userName)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null) { - setUploadCount(Achievements.from(response)); - } else { - Timber.d("success"); - binding.layoutImageReverts.setVisibility(View.INVISIBLE); - binding.achievementBadgeImage.setVisibility(View.INVISIBLE); - // If the number of edits made by the user are more than 150,000 - // in some cases such high number of wiki edit counts cause the - // achievements calculator to fail in some cases, for more details - // refer Issue: #3295 - if (numberOfEdits <= 150000) { - showSnackBarWithRetry(false); - } else { - showSnackBarWithRetry(true); - } - } - }, - t -> { - Timber.e(t, "Fetching achievements statistics failed"); - if (numberOfEdits <= 150000) { - showSnackBarWithRetry(false); - } else { - showSnackBarWithRetry(true); - } - } - )); - } - catch (Exception e){ - Timber.d(e+"success"); - } - } - } - - /** - * To call the API to fetch the count of wiki data edits - * in the form of JavaRx Single object - */ - private void setWikidataEditCount() { - if (StringUtils.isBlank(userName)) { - return; - } - compositeDisposable.add(okHttpJsonApiClient - .getWikidataEdits(userName) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(edits -> { - numberOfEdits = edits; - binding.wikidataEdits.setText(String.valueOf(edits)); - }, e -> { - Timber.e("Error:" + e); - })); - } - - /** - * Shows a snack bar which has an action button which on click dismisses the snackbar and invokes the - * listener passed - * @param tooManyAchievements if this value is true it means that the number of achievements of the - * user are so high that it wrecks havoc with the Achievements calculator due to which request may time - * out. Well this is the Ultimate Achievement - */ - private void showSnackBarWithRetry(boolean tooManyAchievements) { - if (tooManyAchievements) { - binding.progressBar.setVisibility(View.GONE); - ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), - R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements()); - } else { - binding.progressBar.setVisibility(View.GONE); - ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), - R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements()); - } - } - - /** - * Shows a generic error toast when error occurs while loading achievements or uploads - */ - private void onError() { - ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred)); - binding.progressBar.setVisibility(View.GONE); - } - - /** - * used to the count of images uploaded by user - */ - private void setUploadCount(Achievements achievements) { - if (checkAccount()) { - compositeDisposable.add(okHttpJsonApiClient - .getUploadCount(Objects.requireNonNull(userName)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - uploadCount -> setAchievementsUploadCount(achievements, uploadCount), - t -> { - Timber.e(t, "Fetching upload count failed"); - onError(); - } - )); - } - } - - /** - * used to set achievements upload count and call hideProgressbar - * @param uploadCount - */ - private void setAchievementsUploadCount(Achievements achievements, int uploadCount) { - // Create a new instance of Achievements with updated imagesUploaded - Achievements updatedAchievements = new Achievements( - achievements.getUniqueUsedImages(), - achievements.getArticlesUsingImages(), - achievements.getThanksReceived(), - achievements.getFeaturedImages(), - achievements.getQualityImages(), - uploadCount, // Update imagesUploaded with new value - achievements.getRevertCount() - ); - - hideProgressBar(updatedAchievements); - } - - /** - * used to the uploaded images progressbar - * @param uploadCount - */ - private void setUploadProgress(int uploadCount){ - if (uploadCount==0){ - setZeroAchievements(); - }else { - binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE); - binding.imagesUploadedProgressbar.setProgress - (100*uploadCount/levelInfo.getMaxUploadCount()); - binding.tvUploadedImages.setText - (uploadCount + "/" + levelInfo.getMaxUploadCount()); - } - - } - - private void setZeroAchievements() { - String message = !Objects.equals(sessionManager.getUserName(), userName) ? - getString(R.string.no_achievements_yet, userName) : - getString(R.string.you_have_no_achievements_yet); - DialogUtil.showAlertDialog(getActivity(), - null, - message, - getString(R.string.ok), - () -> {}, - true); -// binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE); -// binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE); -// binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE); - binding.achievementBadgeImage.setVisibility(View.INVISIBLE); - binding.imagesUsedByWikiText.setText(R.string.no_image); - binding.imagesRevertedText.setText(R.string.no_image_reverted); - binding.imagesUploadTextParam.setText(R.string.no_image_uploaded); - binding.achievementBadgeImage.setVisibility(View.INVISIBLE); - } - - /** - * used to set the non revert image percentage - * @param notRevertPercentage - */ - private void setImageRevertPercentage(int notRevertPercentage){ - binding.imageRevertsProgressbar.setVisibility(View.VISIBLE); - binding.imageRevertsProgressbar.setProgress(notRevertPercentage); - final String revertPercentage = Integer.toString(notRevertPercentage); - binding.tvRevertedImages.setText(revertPercentage + "%"); - binding.imagesRevertLimitText.setText(getResources().getString(R.string.achievements_revert_limit_message)+ levelInfo.getMinNonRevertPercentage() + "%"); - } - - /** - * Used the inflate the fetched statistics of the images uploaded by user - * and assign badge and level. Also stores the achievements level of the user in BasicKvStore to display in menu - * @param achievements - */ - private void inflateAchievements(Achievements achievements) { -// binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE); - binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived())); - binding.imagesUsedByWikiProgressBar.setProgress - (100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages()); - binding.tvWikiPb.setText(achievements.getUniqueUsedImages() + "/" - + levelInfo.getMaxUniqueImages()); - binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages())); - binding.qualityImages.setText(String.valueOf(achievements.getQualityImages())); - String levelUpInfoString = getString(R.string.level).toUpperCase(Locale.ROOT); - levelUpInfoString += " " + levelInfo.getLevelNumber(); - binding.achievementLevel.setText(levelUpInfoString); - binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge, - new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme())); - binding.achievementBadgeText.setText(Integer.toString(levelInfo.getLevelNumber())); - BasicKvStore store = new BasicKvStore(this.getContext(), userName); - store.putString("userAchievementsLevel", Integer.toString(levelInfo.getLevelNumber())); - } - - /** - * to hide progressbar - */ - private void hideProgressBar(Achievements achievements) { - if (binding.progressBar != null) { - levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(), - achievements.getUniqueUsedImages(), - achievements.getNotRevertPercentage()); - inflateAchievements(achievements); - setUploadProgress(achievements.getImagesUploaded()); - setImageRevertPercentage(achievements.getNotRevertPercentage()); - binding.progressBar.setVisibility(View.GONE); - } - } - - protected void showUploadInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.images_uploaded), - getResources().getString(R.string.images_uploaded_explanation), - IMAGES_UPLOADED_URL); - } - - protected void showRevertedInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.image_reverts), - getResources().getString(R.string.images_reverted_explanation), - IMAGES_REVERT_URL); - } - - protected void showUsedByWikiInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.images_used_by_wiki), - getResources().getString(R.string.images_used_explanation), - IMAGES_USED_URL); - } - - protected void showImagesViaNearbyInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_wikidata_edits), - getResources().getString(R.string.images_via_nearby_explanation), - IMAGES_NEARBY_PLACES_URL); - } - - protected void showFeaturedImagesInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_featured), - getResources().getString(R.string.images_featured_explanation), - IMAGES_FEATURED_URL); - } - - protected void showThanksReceivedInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_thanks), - getResources().getString(R.string.thanks_received_explanation), - THANKS_URL); - } - - public void showQualityImagesInfo() { - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_quality), - getResources().getString(R.string.quality_images_info), - QUALITY_IMAGE_URL); - } - - /** - * takes title and message as input to display alerts - * @param title - * @param message - */ - private void launchAlert(String title, String message){ - DialogUtil.showAlertDialog(getActivity(), - title, - message, - getString(R.string.ok), - () -> {}, - true); - } - - /** - * Launch Alert with a READ MORE button and clicking it open a custom webpage - */ - private void launchAlertWithHelpLink(String title, String message, String helpLinkUrl) { - DialogUtil.showAlertDialog(getActivity(), - title, - message, - getString(R.string.ok), - getString(R.string.read_help_link), - () -> {}, - () -> Utils.handleWebUrl(requireContext(), Uri.parse(helpLinkUrl)), - null, - true); - } - - /** - * check to ensure that user is logged in - * @return - */ - private boolean checkAccount(){ - Account currentAccount = sessionManager.getCurrentAccount(); - if (currentAccount == null) { - Timber.d("Current account is null"); - ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.user_not_logged_in)); - sessionManager.forceLogin(getActivity()); - return false; - } - return true; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt new file mode 100644 index 0000000000..020a67f24f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt @@ -0,0 +1,566 @@ +package fr.free.nrw.commons.profile.achievements + +import android.net.Uri +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.widget.Toast +import androidx.appcompat.view.ContextThemeWrapper +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat +import com.google.android.material.badge.BadgeDrawable +import com.google.android.material.badge.BadgeUtils +import com.google.android.material.badge.ExperimentalBadgeUtils +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.databinding.FragmentAchievementsBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.kvstore.BasicKvStore +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.profile.achievements.LevelController.LevelInfo.Companion.from +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog +import fr.free.nrw.commons.utils.ViewUtil.showDismissibleSnackBar +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import org.apache.commons.lang3.StringUtils +import timber.log.Timber +import java.util.Objects +import javax.inject.Inject + +class AchievementsFragment : CommonsDaggerSupportFragment(){ + private lateinit var levelInfo: LevelController.LevelInfo + + @Inject + lateinit var sessionManager: SessionManager + + @Inject + lateinit var okHttpJsonApiClient: OkHttpJsonApiClient + + private var _binding: FragmentAchievementsBinding? = null + private val binding get() = _binding!! + // To keep track of the number of wiki edits made by a user + private var numberOfEdits: Int = 0 + + private var userName: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + userName = it.getString(ProfileActivity.KEY_USERNAME) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAchievementsBinding.inflate(inflater, container, false) + + binding.achievementInfo.setOnClickListener { showInfoDialog() } + binding.imagesUploadInfoIcon.setOnClickListener { showUploadInfo() } + binding.imagesRevertedInfoIcon.setOnClickListener { showRevertedInfo() } + binding.imagesUsedByWikiInfoIcon.setOnClickListener { showUsedByWikiInfo() } + binding.wikidataEditsIcon.setOnClickListener { showImagesViaNearbyInfo() } + binding.featuredImageIcon.setOnClickListener { showFeaturedImagesInfo() } + binding.thanksImageIcon.setOnClickListener { showThanksReceivedInfo() } + binding.qualityImageIcon.setOnClickListener { showQualityImagesInfo() } + + // DisplayMetrics used to fetch the size of the screen + val displayMetrics = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) + val height = displayMetrics.heightPixels + val width = displayMetrics.widthPixels + + // Used for the setting the size of imageView at runtime + // TODO REMOVE + val params = binding.achievementBadgeImage.layoutParams as ConstraintLayout.LayoutParams + params.height = (height * BADGE_IMAGE_HEIGHT_RATIO).toInt() + params.width = (width * BADGE_IMAGE_WIDTH_RATIO).toInt() + binding.achievementBadgeImage.requestLayout() + binding.progressBar.visibility = View.VISIBLE + + setHasOptionsMenu(true) + if (sessionManager.userName == null || sessionManager.userName == userName) { + binding.tvAchievementsOfUser.visibility = View.GONE + } else { + binding.tvAchievementsOfUser.visibility = View.VISIBLE + binding.tvAchievementsOfUser.text = getString(R.string.achievements_of_user, userName) + } + if (isBetaFlavour) { + binding.layout.visibility = View.GONE + setMenuVisibility(true) + return binding.root + } + + + setWikidataEditCount() + setAchievements() + return binding.root + + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + + override fun setMenuVisibility(visible: Boolean) { + super.setMenuVisibility(visible) + + // Whenever this fragment is revealed in a menu, + // notify Beta users the page data is unavailable + if (isBetaFlavour && visible) { + val ctx = context ?: view?.context + ctx?.let { + Toast.makeText(it, R.string.achievements_unavailable_beta, Toast.LENGTH_LONG).show() + } + } + } + + /** + * To invoke the AlertDialog on clicking info button + */ + fun showInfoDialog() { + launchAlert( + resources.getString(R.string.Achievements), + resources.getString(R.string.achievements_info_message) + ) + } + + + + + /** + * To call the API to get results in form Single + * which then calls parseJson when results are fetched + */ + + private fun setAchievements() { + binding.progressBar.visibility = View.VISIBLE + if (checkAccount()) { + try { + compositeDisposable.add( + okHttpJsonApiClient + .getAchievements(userName ?: return) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + if (response != null) { + setUploadCount(Achievements.from(response)) + } else { + Timber.d("Success") + // TODO Create a Method to Hide all the Statistics +// binding.layoutImageReverts.visibility = View.INVISIBLE +// binding.achievementBadgeImage.visibility = View.INVISIBLE + // If the number of edits made by the user are more than 150,000 + // in some cases such high number of wiki edit counts cause the + // achievements calculator to fail in some cases, for more details + // refer Issue: #3295 + if (numberOfEdits <= 150_000) { + showSnackBarWithRetry(false) + } else { + showSnackBarWithRetry(true) + } + } + }, + { throwable -> + Timber.e(throwable, "Fetching achievements statistics failed") + if (numberOfEdits <= 150_000) { + showSnackBarWithRetry(false) + } else { + showSnackBarWithRetry(true) + } + } + ) + ) + } catch (e: Exception) { + Timber.d("Exception: ${e.message}") + } + } + } + + /** + * To call the API to fetch the count of wiki data edits + * in the form of JavaRx Single object + */ + + private fun setWikidataEditCount() { + if (StringUtils.isBlank(userName)) { + return + } + compositeDisposable.add( + okHttpJsonApiClient + .getWikidataEdits(userName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ edits: Int -> + numberOfEdits = edits + showBadgesWithCount(view = binding.wikidataEditsIcon, count = edits) + }, { e: Throwable -> + Timber.e("Error:$e") + }) + ) + } + + /** + * Shows a snack bar which has an action button which on click dismisses the snackbar and invokes the + * listener passed + * @param tooManyAchievements if this value is true it means that the number of achievements of the + * user are so high that it wrecks havoc with the Achievements calculator due to which request may time + * out. Well this is the Ultimate Achievement + */ + private fun showSnackBarWithRetry(tooManyAchievements: Boolean) { + if (tooManyAchievements) { + if (view == null) { + return + } + else { + binding.progressBar.visibility = View.GONE + showDismissibleSnackBar( + requireView().findViewById(android.R.id.content), + R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry + ) { setAchievements() } + } + + } else { + if (view == null) { + return + } + binding.progressBar.visibility = View.GONE + showDismissibleSnackBar( + requireView().findViewById(android.R.id.content), + R.string.achievements_fetch_failed, R.string.retry + ) { setAchievements() } + } + } + + /** + * Shows a generic error toast when error occurs while loading achievements or uploads + */ + private fun onError() { + showLongToast(requireActivity(), resources.getString(R.string.error_occurred)) + binding.progressBar.visibility = View.GONE + } + + /** + * used to the count of images uploaded by user + */ + + private fun setUploadCount(achievements: Achievements) { + if (checkAccount()) { + compositeDisposable.add(okHttpJsonApiClient + .getUploadCount(Objects.requireNonNull(userName)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { uploadCount: Int? -> + setAchievementsUploadCount( + achievements, + uploadCount ?:0 + ) + }, + { t: Throwable? -> + Timber.e(t, "Fetching upload count failed") + onError() + } + )) + } + } + + /** + * used to set achievements upload count and call hideProgressbar + * @param uploadCount + */ + private fun setAchievementsUploadCount(achievements: Achievements, uploadCount: Int) { + // Create a new instance of Achievements with updated imagesUploaded + val updatedAchievements = Achievements( + achievements.uniqueUsedImages, + achievements.articlesUsingImages, + achievements.thanksReceived, + achievements.featuredImages, + achievements.qualityImages, + uploadCount, // Update imagesUploaded with new value + achievements.revertCount + ) + + hideProgressBar(updatedAchievements) + } + + /** + * used to the uploaded images progressbar + * @param uploadCount + */ + private fun setUploadProgress(uploadCount: Int) { + if (uploadCount == 0) { + setZeroAchievements() + } else { + binding.imagesUploadedProgressbar.visibility = View.VISIBLE + binding.imagesUploadedProgressbar.progress = + 100 * uploadCount / levelInfo.maxUploadCount + binding.imageUploadedTVCount.text = uploadCount.toString() + "/" + levelInfo.maxUploadCount + } + } + + private fun setZeroAchievements() { + val message = if (sessionManager.userName != userName) { + getString(R.string.no_achievements_yet, userName ) + } else { + getString(R.string.you_have_no_achievements_yet) + } + showAlertDialog( + requireActivity(), + null, + message, + getString(R.string.ok), + {}, + true + ) + +// binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE); +// binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE); +// binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE); + //binding.achievementBadgeImage.visibility = View.INVISIBLE // TODO + binding.imagesUsedCount.setText(R.string.no_image) + binding.imagesRevertedText.setText(R.string.no_image_reverted) + binding.imagesUploadTextParam.setText(R.string.no_image_uploaded) + } + + /** + * used to set the non revert image percentage + * @param notRevertPercentage + */ + private fun setImageRevertPercentage(notRevertPercentage: Int) { + binding.imageRevertsProgressbar.visibility = View.VISIBLE + binding.imageRevertsProgressbar.progress = notRevertPercentage + val revertPercentage = notRevertPercentage.toString() + binding.imageRevertTVCount.text = "$revertPercentage%" + binding.imagesRevertLimitText.text = + resources.getString(R.string.achievements_revert_limit_message) + levelInfo.minNonRevertPercentage + "%" + } + + /** + * Used the inflate the fetched statistics of the images uploaded by user + * and assign badge and level. Also stores the achievements level of the user in BasicKvStore to display in menu + * @param achievements + */ + private fun inflateAchievements(achievements: Achievements) { + + // Thanks Received Badge + showBadgesWithCount(view = binding.thanksImageIcon, count = achievements.thanksReceived) + + // Featured Images Badge + showBadgesWithCount(view = binding.featuredImageIcon, count = achievements.featuredImages) + + // Quality Images Badge + showBadgesWithCount(view = binding.qualityImageIcon, count = achievements.qualityImages) + + binding.imagesUsedByWikiProgressBar.progress = + 100 * achievements.uniqueUsedImages / levelInfo.maxUniqueImages + binding.imagesUsedCount.text = (achievements.uniqueUsedImages.toString() + "/" + + levelInfo.maxUniqueImages) + + binding.achievementLevel.text = getString(R.string.level,levelInfo.levelNumber) + binding.achievementBadgeImage.setImageDrawable( + VectorDrawableCompat.create( + resources, R.drawable.badge, + ContextThemeWrapper(activity, levelInfo.levelStyle).theme + ) + ) + binding.achievementBadgeText.text = levelInfo.levelNumber.toString() + val store = BasicKvStore(requireContext(), userName) + store.putString("userAchievementsLevel", levelInfo.levelNumber.toString()) + } + + /** + * This function is used to show badge on any view (button, imageView, etc) + * @param view The View on which the badge will be displayed eg (button, imageView, etc) + * @param count The number to be displayed inside the badge. + * @param backgroundColor The badge background color. Default is R.attr.colorPrimary + * @param badgeTextColor The badge text color. Default is R.attr.colorPrimary + * @param badgeGravity The position of the badge [TOP_END,TOP_START,BOTTOM_END,BOTTOM_START]. Default is TOP_END + * @return if the number is 0, then it will not create badge for it and hide the view + * @see https://developer.android.com/reference/com/google/android/material/badge/BadgeDrawable + */ + + private fun showBadgesWithCount( + view: View, + count: Int, + backgroundColor: Int = R.attr.colorPrimary, + badgeTextColor: Int = R.attr.textEnabled, + badgeGravity: Int = BadgeDrawable.TOP_END + ) { + //https://stackoverflow.com/a/67742035 + if (count == 0) { + view.visibility = View.GONE + return + } + + view.viewTreeObserver.addOnGlobalLayoutListener(object : + ViewTreeObserver.OnGlobalLayoutListener { + /** + * Callback method to be invoked when the global layout state or the visibility of views + * within the view tree changes + */ + @ExperimentalBadgeUtils + override fun onGlobalLayout() { + view.visibility = View.VISIBLE + val badgeDrawable = BadgeDrawable.create(requireActivity()) + badgeDrawable.number = count + badgeDrawable.badgeGravity = badgeGravity + badgeDrawable.badgeTextColor = badgeTextColor + badgeDrawable.backgroundColor = backgroundColor + BadgeUtils.attachBadgeDrawable(badgeDrawable, view) + view.getViewTreeObserver().removeOnGlobalLayoutListener(this) + } + }) + } + + /** + * to hide progressbar + */ + private fun hideProgressBar(achievements: Achievements) { + if (binding.progressBar != null) { + levelInfo = from( + achievements.imagesUploaded, + achievements.uniqueUsedImages, + achievements.notRevertPercentage + ) + inflateAchievements(achievements) + setUploadProgress(achievements.imagesUploaded) + setImageRevertPercentage(achievements.notRevertPercentage) + binding.progressBar.visibility = View.GONE + } + } + + fun showUploadInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.images_uploaded), + resources.getString(R.string.images_uploaded_explanation), + IMAGES_UPLOADED_URL + ) + } + + fun showRevertedInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.image_reverts), + resources.getString(R.string.images_reverted_explanation), + IMAGES_REVERT_URL + ) + } + + fun showUsedByWikiInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.images_used_by_wiki), + resources.getString(R.string.images_used_explanation), + IMAGES_USED_URL + ) + } + + fun showImagesViaNearbyInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_wikidata_edits), + resources.getString(R.string.images_via_nearby_explanation), + IMAGES_NEARBY_PLACES_URL + ) + } + + fun showFeaturedImagesInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_featured), + resources.getString(R.string.images_featured_explanation), + IMAGES_FEATURED_URL + ) + } + + fun showThanksReceivedInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_thanks), + resources.getString(R.string.thanks_received_explanation), + THANKS_URL + ) + } + + fun showQualityImagesInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_quality), + resources.getString(R.string.quality_images_info), + QUALITY_IMAGE_URL + ) + } + + /** + * takes title and message as input to display alerts + * @param title + * @param message + */ + private fun launchAlert(title: String, message: String) { + showAlertDialog( + requireActivity(), + title, + message, + getString(R.string.ok), + {}, + true + ) + } + + /** + * Launch Alert with a READ MORE button and clicking it open a custom webpage + */ + private fun launchAlertWithHelpLink(title: String, message: String, helpLinkUrl: String) { + showAlertDialog( + requireActivity(), + title, + message, + getString(R.string.ok), + getString(R.string.read_help_link), + {}, + { Utils.handleWebUrl(requireContext(), Uri.parse(helpLinkUrl)) }, + null, + true + ) + } + /** + * check to ensure that user is logged in + * @return + */ + private fun checkAccount(): Boolean { + val currentAccount = sessionManager.currentAccount + if (currentAccount == null) { + Timber.d("Current account is null") + showLongToast(requireActivity(), resources.getString(R.string.user_not_logged_in)) + sessionManager.forceLogin(activity) + return false + } + return true + } + + + + companion object{ + private const val BADGE_IMAGE_WIDTH_RATIO = 0.4 + private const val BADGE_IMAGE_HEIGHT_RATIO = 0.3 + + /** + * Help link URLs + */ + private const val IMAGES_UPLOADED_URL = "https://commons.wikimedia.org/wiki/Commons:Project_scope" + private const val IMAGES_REVERT_URL = "https://commons.wikimedia.org/wiki/Commons:Deletion_policy#Reasons_for_deletion" + private const val IMAGES_USED_URL = "https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images" + private const val IMAGES_NEARBY_PLACES_URL = "https://www.wikidata.org/wiki/Property:P18" + private const val IMAGES_FEATURED_URL = "https://commons.wikimedia.org/wiki/Commons:Featured_pictures" + private const val QUALITY_IMAGE_URL = "https://commons.wikimedia.org/wiki/Commons:Quality_images" + private const val THANKS_URL = "https://www.mediawiki.org/wiki/Extension:Thanks" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_achievements.xml b/app/src/main/res/layout/fragment_achievements.xml index e0dddcf5bf..00c18b3232 100644 --- a/app/src/main/res/layout/fragment_achievements.xml +++ b/app/src/main/res/layout/fragment_achievements.xml @@ -1,640 +1,368 @@ - - - + + + + + + + + + - - + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_centerInParent="true" + android:progressDrawable="@android:drawable/progress_horizontal" + android:progressBackgroundTintMode="multiply" + android:progressTint="#5ce65c" + tools:progress="50" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 187c5fc96f..fa21736990 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -371,11 +371,13 @@ Delete Achievements Profile + Badges Statistics Thanks Received Featured Images Images via \"Nearby Places\" - Level + Level %d + %s (Level %s) Images Uploaded Images Not Reverted Images Used diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 94856e4eb6..67b5eae0f5 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,6 +1,6 @@ - -