diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java deleted file mode 100644 index 409450d60..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java +++ /dev/null @@ -1,125 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING; - -import androidx.annotation.NonNull; -import androidx.lifecycle.MutableLiveData; -import androidx.paging.PageKeyedDataSource; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.disposables.CompositeDisposable; -import java.util.Objects; -import timber.log.Timber; - -/** - * This class will call the leaderboard API to get new list when the pagination is performed - */ -public class DataSourceClass extends PageKeyedDataSource { - - private OkHttpJsonApiClient okHttpJsonApiClient; - private SessionManager sessionManager; - private MutableLiveData progressLiveStatus; - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - private String duration; - private String category; - private int limit; - private int offset; - - /** - * Initialise the Data Source Class with API params - * @param okHttpJsonApiClient - * @param sessionManager - * @param duration - * @param category - * @param limit - * @param offset - */ - public DataSourceClass(OkHttpJsonApiClient okHttpJsonApiClient,SessionManager sessionManager, - String duration, String category, int limit, int offset) { - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.sessionManager = sessionManager; - this.duration = duration; - this.category = category; - this.limit = limit; - this.offset = offset; - progressLiveStatus = new MutableLiveData<>(); - } - - - /** - * @return the status of the list - */ - public MutableLiveData getProgressLiveStatus() { - return progressLiveStatus; - } - - /** - * Loads the initial set of data from API - * @param params - * @param callback - */ - @Override - public void loadInitial(@NonNull LoadInitialParams params, - @NonNull LoadInitialCallback callback) { - - compositeDisposable.add(okHttpJsonApiClient - .getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name, - duration, category, String.valueOf(limit), String.valueOf(offset)) - .doOnSubscribe(disposable -> { - compositeDisposable.add(disposable); - progressLiveStatus.postValue(LOADING); - }).subscribe( - response -> { - if (response != null && response.getStatus() == 200) { - progressLiveStatus.postValue(LOADED); - callback.onResult(response.getLeaderboardList(), null, response.getLimit()); - } - }, - t -> { - Timber.e(t, "Fetching leaderboard statistics failed"); - progressLiveStatus.postValue(LOADING); - } - )); - - } - - /** - * Loads any data before the inital page is loaded - * @param params - * @param callback - */ - @Override - public void loadBefore(@NonNull LoadParams params, - @NonNull LoadCallback callback) { - - } - - /** - * Loads the next set of data on scrolling with offset as the limit of the last set of data - * @param params - * @param callback - */ - @Override - public void loadAfter(@NonNull LoadParams params, - @NonNull LoadCallback callback) { - compositeDisposable.add(okHttpJsonApiClient - .getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name, - duration, category, String.valueOf(limit), String.valueOf(params.key)) - .doOnSubscribe(disposable -> { - compositeDisposable.add(disposable); - progressLiveStatus.postValue(LOADING); - }).subscribe( - response -> { - if (response != null && response.getStatus() == 200) { - progressLiveStatus.postValue(LOADED); - callback.onResult(response.getLeaderboardList(), params.key + limit); - } - }, - t -> { - Timber.e(t, "Fetching leaderboard statistics failed"); - progressLiveStatus.postValue(LOADING); - } - )); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.kt new file mode 100644 index 000000000..a6fe747e5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.kt @@ -0,0 +1,79 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.accounts.Account +import androidx.lifecycle.MutableLiveData +import androidx.paging.PageKeyedDataSource +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADING +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADED +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import timber.log.Timber +import java.util.Objects + +/** + * This class will call the leaderboard API to get new list when the pagination is performed + */ +class DataSourceClass( + private val okHttpJsonApiClient: OkHttpJsonApiClient, + private val sessionManager: SessionManager, + private val duration: String?, + private val category: String?, + private val limit: Int, + private val offset: Int +) : PageKeyedDataSource() { + val progressLiveStatus: MutableLiveData = MutableLiveData() + private val compositeDisposable = CompositeDisposable() + + + override fun loadInitial( + params: LoadInitialParams, callback: LoadInitialCallback + ) { + compositeDisposable.add(okHttpJsonApiClient.getLeaderboard( + sessionManager.currentAccount?.name, + duration, + category, + limit.toString(), + offset.toString() + ).doOnSubscribe { disposable: Disposable? -> + compositeDisposable.add(disposable!!) + progressLiveStatus.postValue(LOADING) + }.subscribe({ response: LeaderboardResponse? -> + if (response != null && response.status == 200) { + progressLiveStatus.postValue(LOADED) + callback.onResult(response.leaderboardList!!, null, response.limit) + } + }, { t: Throwable? -> + Timber.e(t, "Fetching leaderboard statistics failed") + progressLiveStatus.postValue(LOADING) + })) + } + + override fun loadBefore( + params: LoadParams, callback: LoadCallback + ) = Unit + + override fun loadAfter( + params: LoadParams, callback: LoadCallback + ) { + compositeDisposable.add(okHttpJsonApiClient.getLeaderboard( + Objects.requireNonNull(sessionManager.currentAccount).name, + duration, + category, + limit.toString(), + params.key.toString() + ).doOnSubscribe { disposable: Disposable? -> + compositeDisposable.add(disposable!!) + progressLiveStatus.postValue(LOADING) + }.subscribe({ response: LeaderboardResponse? -> + if (response != null && response.status == 200) { + progressLiveStatus.postValue(LOADED) + callback.onResult(response.leaderboardList!!, params.key + limit) + } + }, { t: Throwable? -> + Timber.e(t, "Fetching leaderboard statistics failed") + progressLiveStatus.postValue(LOADING) + })) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java deleted file mode 100644 index b2965785a..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java +++ /dev/null @@ -1,110 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import androidx.lifecycle.MutableLiveData; -import androidx.paging.DataSource; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.disposables.CompositeDisposable; - -/** - * This class will create a new instance of the data source class on pagination - */ -public class DataSourceFactory extends DataSource.Factory { - - private MutableLiveData liveData; - private OkHttpJsonApiClient okHttpJsonApiClient; - private CompositeDisposable compositeDisposable; - private SessionManager sessionManager; - private String duration; - private String category; - private int limit; - private int offset; - - /** - * Gets the current set leaderboard list duration - */ - public String getDuration() { - return duration; - } - - /** - * Sets the current set leaderboard duration with the new duration - */ - public void setDuration(final String duration) { - this.duration = duration; - } - - /** - * Gets the current set leaderboard list category - */ - public String getCategory() { - return category; - } - - /** - * Sets the current set leaderboard category with the new category - */ - public void setCategory(final String category) { - this.category = category; - } - - /** - * Gets the current set leaderboard list limit - */ - public int getLimit() { - return limit; - } - - /** - * Sets the current set leaderboard limit with the new limit - */ - public void setLimit(final int limit) { - this.limit = limit; - } - - /** - * Gets the current set leaderboard list offset - */ - public int getOffset() { - return offset; - } - - /** - * Sets the current set leaderboard offset with the new offset - */ - public void setOffset(final int offset) { - this.offset = offset; - } - - /** - * Constructor for DataSourceFactory class - * @param okHttpJsonApiClient client for OKhttp - * @param compositeDisposable composite disposable - * @param sessionManager sessionManager - */ - public DataSourceFactory(OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable, - SessionManager sessionManager) { - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.compositeDisposable = compositeDisposable; - this.sessionManager = sessionManager; - liveData = new MutableLiveData<>(); - } - - /** - * @return the live data - */ - public MutableLiveData getMutableLiveData() { - return liveData; - } - - /** - * Creates the new instance of data source class - * @return - */ - @Override - public DataSource create() { - DataSourceClass dataSourceClass = new DataSourceClass(okHttpJsonApiClient, sessionManager, duration, category, limit, offset); - liveData.postValue(dataSourceClass); - return dataSourceClass; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.kt new file mode 100644 index 000000000..6e979d8c3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.kt @@ -0,0 +1,27 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.lifecycle.MutableLiveData +import androidx.paging.DataSource +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient + +/** + * This class will create a new instance of the data source class on pagination + */ +class DataSourceFactory( + private val okHttpJsonApiClient: OkHttpJsonApiClient, + private val sessionManager: SessionManager +) : DataSource.Factory() { + val mutableLiveData: MutableLiveData = MutableLiveData() + var duration: String? = null + var category: String? = null + var limit: Int = 0 + var offset: Int = 0 + + /** + * Creates the new instance of data source class + */ + override fun create(): DataSource = DataSourceClass( + okHttpJsonApiClient, sessionManager, duration, category, limit, offset + ).also { mutableLiveData.postValue(it) } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java deleted file mode 100644 index 800287f4f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java +++ /dev/null @@ -1,45 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -/** - * This class contains the constant variables for leaderboard - */ -public class LeaderboardConstants { - - /** - * This is the size of the page i.e. number items to load in a batch when pagination is performed - */ - public static final int PAGE_SIZE = 100; - - /** - * This is the starting offset, we set it to 0 to start loading from rank 1 - */ - public static final int START_OFFSET = 0; - - /** - * This is the prefix of the user's homepage url, appending the username will give us complete url - */ - public static final String USER_LINK_PREFIX = "https://commons.wikimedia.org/wiki/User:"; - - /** - * This is the a constant string for the state loading, when the pages are getting loaded we can - * use this constant to identify if we need to show the progress bar or not - */ - public final static String LOADING = "Loading"; - - /** - * This is the a constant string for the state loaded, when the pages are loaded we can - * use this constant to identify if we need to show the progress bar or not - */ - public final static String LOADED = "Loaded"; - - /** - * This API endpoint is to update the leaderboard avatar - */ - public final static String UPDATE_AVATAR_END_POINT = "/update_avatar.py"; - - /** - * This API endpoint is to get leaderboard data - */ - public final static String LEADERBOARD_END_POINT = "/leaderboard.py"; - -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.kt new file mode 100644 index 000000000..bf8d45c5f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.kt @@ -0,0 +1,44 @@ +package fr.free.nrw.commons.profile.leaderboard + +/** + * This class contains the constant variables for leaderboard + */ +object LeaderboardConstants { + /** + * This is the size of the page i.e. number items to load in a batch when pagination is performed + */ + const val PAGE_SIZE: Int = 100 + + /** + * This is the starting offset, we set it to 0 to start loading from rank 1 + */ + const val START_OFFSET: Int = 0 + + /** + * This is the prefix of the user's homepage url, appending the username will give us complete url + */ + const val USER_LINK_PREFIX: String = "https://commons.wikimedia.org/wiki/User:" + + sealed class LoadingStatus { + /** + * This is the state loading, when the pages are getting loaded we can + * use this constant to identify if we need to show the progress bar or not + */ + data object LOADING: LoadingStatus() + /** + * This is the state loaded, when the pages are loaded we can + * use this constant to identify if we need to show the progress bar or not + */ + data object LOADED: LoadingStatus() + } + + /** + * This API endpoint is to update the leaderboard avatar + */ + const val UPDATE_AVATAR_END_POINT: String = "/update_avatar.py" + + /** + * This API endpoint is to get leaderboard data + */ + const val LEADERBOARD_END_POINT: String = "/leaderboard.py" +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java deleted file mode 100644 index a9cc222ea..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java +++ /dev/null @@ -1,363 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.START_OFFSET; - -import android.accounts.Account; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.ArrayAdapter; -import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.MergeAdapter; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.databinding.FragmentLeaderboardBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.profile.ProfileActivity; -import fr.free.nrw.commons.utils.ConfigUtils; -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.Objects; -import javax.inject.Inject; -import timber.log.Timber; - -/** - * This class extends the CommonsDaggerSupportFragment and creates leaderboard fragment - */ -public class LeaderboardFragment extends CommonsDaggerSupportFragment { - - - @Inject - SessionManager sessionManager; - - @Inject - OkHttpJsonApiClient okHttpJsonApiClient; - - @Inject - ViewModelFactory viewModelFactory; - - /** - * View model for the paged leaderboard list - */ - private LeaderboardListViewModel viewModel; - - /** - * Composite disposable for API call - */ - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - /** - * Duration of the leaderboard API - */ - private String duration; - - /** - * Category of the Leaderboard API - */ - private String category; - - /** - * Page size of the leaderboard API - */ - private int limit = PAGE_SIZE; - - /** - * offset for the leaderboard API - */ - private int offset = START_OFFSET; - - /** - * Set initial User Rank to 0 - */ - private int userRank; - - /** - * This variable represents if user wants to scroll to his rank or not - */ - private boolean scrollToRank; - - private String userName; - - private FragmentLeaderboardBinding binding; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getArguments() != null) { - userName = getArguments().getString(ProfileActivity.KEY_USERNAME); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentLeaderboardBinding.inflate(inflater, container, false); - - hideLayouts(); - - // Leaderboard currently unimplemented in Beta flavor. Skip all API calls and disable menu - if(ConfigUtils.isBetaFlavour()) { - binding.progressBar.setVisibility(View.GONE); - binding.scroll.setVisibility(View.GONE); - return binding.getRoot(); - } - - binding.progressBar.setVisibility(View.VISIBLE); - setSpinners(); - - /** - * This array is for the duration filter, we have three filters weekly, yearly and all-time - * each filter have a key and value pair, the value represents the param of the API - */ - String[] durationValues = getContext().getResources().getStringArray(R.array.leaderboard_duration_values); - - /** - * This array is for the category filter, we have three filters upload, used and nearby - * each filter have a key and value pair, the value represents the param of the API - */ - String[] categoryValues = getContext().getResources().getStringArray(R.array.leaderboard_category_values); - - duration = durationValues[0]; - category = categoryValues[0]; - - setLeaderboard(duration, category, limit, offset); - - binding.durationSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - - duration = durationValues[binding.durationSpinner.getSelectedItemPosition()]; - refreshLeaderboard(); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - } - }); - - binding.categorySpinner.setOnItemSelectedListener(new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - category = categoryValues[binding.categorySpinner.getSelectedItemPosition()]; - refreshLeaderboard(); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - } - }); - - - binding.scroll.setOnClickListener(view -> scrollToUserRank()); - - - return binding.getRoot(); - } - - @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.leaderboard_unavailable_beta, - Toast.LENGTH_LONG).show(); - } - } - } - - /** - * Refreshes the leaderboard list - */ - private void refreshLeaderboard() { - scrollToRank = false; - if (viewModel != null) { - viewModel.refresh(duration, category, limit, offset); - setLeaderboard(duration, category, limit, offset); - } - } - - /** - * Performs Auto Scroll to the User's Rank - * We use userRank+1 to load one extra user and prevent overlapping of my rank button - * If you are viewing the leaderboard below userRank, it scrolls to the user rank at the top - */ - private void scrollToUserRank() { - - if(userRank==0){ - Toast.makeText(getContext(),R.string.no_achievements_yet,Toast.LENGTH_SHORT).show(); - }else { - if (binding == null) { - return; - } - if (Objects.requireNonNull(binding.leaderboardList.getAdapter()).getItemCount() - > userRank + 1) { - binding.leaderboardList.smoothScrollToPosition(userRank + 1); - } else { - if (viewModel != null) { - viewModel.refresh(duration, category, userRank + 1, 0); - setLeaderboard(duration, category, userRank + 1, 0); - scrollToRank = true; - } - } - } - - } - - /** - * Set the spinners for the leaderboard filters - */ - private void setSpinners() { - ArrayAdapter categoryAdapter = ArrayAdapter.createFromResource(getContext(), - R.array.leaderboard_categories, android.R.layout.simple_spinner_item); - categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - binding.categorySpinner.setAdapter(categoryAdapter); - - ArrayAdapter durationAdapter = ArrayAdapter.createFromResource(getContext(), - R.array.leaderboard_durations, android.R.layout.simple_spinner_item); - durationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - binding.durationSpinner.setAdapter(durationAdapter); - } - - /** - * To call the API to get results - * which then sets the views using setLeaderboardUser method - */ - private void setLeaderboard(String duration, String category, int limit, int offset) { - if (checkAccount()) { - try { - compositeDisposable.add(okHttpJsonApiClient - .getLeaderboard(Objects.requireNonNull(userName), - duration, category, null, null) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null && response.getStatus() == 200) { - userRank = response.getRank(); - setViews(response, duration, category, limit, offset); - } - }, - t -> { - Timber.e(t, "Fetching leaderboard statistics failed"); - onError(); - } - )); - } - catch (Exception e){ - Timber.d(e+"success"); - } - } - } - - /** - * Set the views - * @param response Leaderboard Response Object - */ - private void setViews(LeaderboardResponse response, String duration, String category, int limit, int offset) { - viewModel = new ViewModelProvider(this, viewModelFactory).get(LeaderboardListViewModel.class); - viewModel.setParams(duration, category, limit, offset); - LeaderboardListAdapter leaderboardListAdapter = new LeaderboardListAdapter(); - UserDetailAdapter userDetailAdapter= new UserDetailAdapter(response); - MergeAdapter mergeAdapter = new MergeAdapter(userDetailAdapter, leaderboardListAdapter); - LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext()); - binding.leaderboardList.setLayoutManager(linearLayoutManager); - binding.leaderboardList.setAdapter(mergeAdapter); - viewModel.getListLiveData().observe(getViewLifecycleOwner(), leaderboardListAdapter::submitList); - viewModel.getProgressLoadStatus().observe(getViewLifecycleOwner(), status -> { - if (Objects.requireNonNull(status).equalsIgnoreCase(LOADING)) { - showProgressBar(); - } else if (status.equalsIgnoreCase(LOADED)) { - hideProgressBar(); - if (scrollToRank) { - binding.leaderboardList.smoothScrollToPosition(userRank + 1); - } - } - }); - } - - /** - * to hide progressbar - */ - private void hideProgressBar() { - if (binding != null) { - binding.progressBar.setVisibility(View.GONE); - binding.categorySpinner.setVisibility(View.VISIBLE); - binding.durationSpinner.setVisibility(View.VISIBLE); - binding.scroll.setVisibility(View.VISIBLE); - binding.leaderboardList.setVisibility(View.VISIBLE); - } - } - - /** - * to show progressbar - */ - private void showProgressBar() { - if (binding != null) { - binding.progressBar.setVisibility(View.VISIBLE); - binding.scroll.setVisibility(View.INVISIBLE); - } - } - - /** - * used to hide the layouts while fetching results from api - */ - private void hideLayouts(){ - binding.categorySpinner.setVisibility(View.INVISIBLE); - binding.durationSpinner.setVisibility(View.INVISIBLE); - binding.leaderboardList.setVisibility(View.INVISIBLE); - } - - /** - * 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; - } - - /** - * Shows a generic error toast when error occurs while loading leaderboard - */ - private void onError() { - ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred)); - if (binding!=null) { - binding.progressBar.setVisibility(View.GONE); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - compositeDisposable.clear(); - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt new file mode 100644 index 000000000..e77c24c8d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt @@ -0,0 +1,319 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.MergeAdapter +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.databinding.FragmentLeaderboardBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADED +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADING +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.START_OFFSET +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.util.Objects +import javax.inject.Inject + +/** + * This class extends the CommonsDaggerSupportFragment and creates leaderboard fragment + */ +class LeaderboardFragment : CommonsDaggerSupportFragment() { + @Inject + lateinit var sessionManager: SessionManager + + @Inject + lateinit var okHttpJsonApiClient: OkHttpJsonApiClient + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private var viewModel: LeaderboardListViewModel? = null + private var duration: String? = null + private var category: String? = null + private val limit: Int = PAGE_SIZE + private val offset: Int = START_OFFSET + private var userRank = 0 + private var scrollToRank = false + private var userName: String? = null + private var binding: FragmentLeaderboardBinding? = 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 = FragmentLeaderboardBinding.inflate(inflater, container, false) + + hideLayouts() + + // Leaderboard currently unimplemented in Beta flavor. Skip all API calls and disable menu + if (isBetaFlavour) { + binding!!.progressBar.visibility = View.GONE + binding!!.scroll.visibility = View.GONE + return binding!!.root + } + + binding!!.progressBar.visibility = View.VISIBLE + setSpinners() + + /* + * This array is for the duration filter, we have three filters weekly, yearly and all-time + * each filter have a key and value pair, the value represents the param of the API + */ + val durationValues = requireContext().resources + .getStringArray(R.array.leaderboard_duration_values) + duration = durationValues[0] + + /* + * This array is for the category filter, we have three filters upload, used and nearby + * each filter have a key and value pair, the value represents the param of the API + */ + val categoryValues = requireContext().resources + .getStringArray(R.array.leaderboard_category_values) + category = categoryValues[0] + + setLeaderboard(duration, category, limit, offset) + + with(binding!!) { + durationSpinner.onItemSelectedListener = SelectionListener { + duration = durationValues[durationSpinner.selectedItemPosition] + refreshLeaderboard() + } + + categorySpinner.onItemSelectedListener = SelectionListener { + category = categoryValues[categorySpinner.selectedItemPosition] + refreshLeaderboard() + } + + scroll.setOnClickListener { scrollToUserRank() } + + return root + } + } + + 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? = if (context != null) { + context + } else if (view != null && requireView().context != null) { + requireView().context + } else { + null + } + + ctx?.let { + Toast.makeText(it, R.string.leaderboard_unavailable_beta, Toast.LENGTH_LONG).show() + } + } + } + + /** + * Refreshes the leaderboard list + */ + private fun refreshLeaderboard() { + scrollToRank = false + viewModel?.let { + it.refresh(duration, category, limit, offset) + setLeaderboard(duration, category, limit, offset) + } + } + + /** + * Performs Auto Scroll to the User's Rank + * We use userRank+1 to load one extra user and prevent overlapping of my rank button + * If you are viewing the leaderboard below userRank, it scrolls to the user rank at the top + */ + private fun scrollToUserRank() { + if (userRank == 0) { + Toast.makeText(context, R.string.no_achievements_yet, Toast.LENGTH_SHORT).show() + } else { + if (binding == null) { + return + } + val itemCount = binding?.leaderboardList?.adapter?.itemCount ?: 0 + if (itemCount > userRank + 1) { + binding!!.leaderboardList.smoothScrollToPosition(userRank + 1) + } else { + viewModel?.let { + it.refresh(duration, category, userRank + 1, 0) + setLeaderboard(duration, category, userRank + 1, 0) + scrollToRank = true + } + } + } + } + + /** + * Set the spinners for the leaderboard filters + */ + private fun setSpinners() { + val categoryAdapter = ArrayAdapter.createFromResource( + requireContext(), + R.array.leaderboard_categories, android.R.layout.simple_spinner_item + ) + categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding!!.categorySpinner.adapter = categoryAdapter + + val durationAdapter = ArrayAdapter.createFromResource( + requireContext(), + R.array.leaderboard_durations, android.R.layout.simple_spinner_item + ) + durationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding!!.durationSpinner.adapter = durationAdapter + } + + /** + * To call the API to get results + * which then sets the views using setLeaderboardUser method + */ + private fun setLeaderboard(duration: String?, category: String?, limit: Int, offset: Int) { + if (checkAccount()) { + try { + compositeDisposable.add( + okHttpJsonApiClient.getLeaderboard( + Objects.requireNonNull(userName), + duration, category, null, null + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response: LeaderboardResponse? -> + if (response != null && response.status == 200) { + userRank = response.rank!! + setViews(response, duration, category, limit, offset) + } + }, + { t: Throwable? -> + Timber.e(t, "Fetching leaderboard statistics failed") + onError() + } + )) + } catch (e: Exception) { + Timber.d(e, "success") + } + } + } + + /** + * Set the views + * @param response Leaderboard Response Object + */ + private fun setViews( + response: LeaderboardResponse, + duration: String?, + category: String?, + limit: Int, + offset: Int + ) { + viewModel = ViewModelProvider(this, viewModelFactory).get( + LeaderboardListViewModel::class.java + ) + viewModel!!.setParams(duration, category, limit, offset) + val leaderboardListAdapter = LeaderboardListAdapter() + val userDetailAdapter = UserDetailAdapter(response) + val mergeAdapter = MergeAdapter(userDetailAdapter, leaderboardListAdapter) + val linearLayoutManager = LinearLayoutManager(context) + binding!!.leaderboardList.layoutManager = linearLayoutManager + binding!!.leaderboardList.adapter = mergeAdapter + viewModel!!.listLiveData.observe(viewLifecycleOwner, leaderboardListAdapter::submitList) + + viewModel!!.progressLoadStatus.observe(viewLifecycleOwner) { status -> + when (status) { + LOADING -> { + showProgressBar() + } + LOADED -> { + hideProgressBar() + if (scrollToRank) { + binding!!.leaderboardList.smoothScrollToPosition(userRank + 1) + } + } + } + } + } + + /** + * to hide progressbar + */ + private fun hideProgressBar() = binding?.let { + it.progressBar.visibility = View.GONE + it.categorySpinner.visibility = View.VISIBLE + it.durationSpinner.visibility = View.VISIBLE + it.scroll.visibility = View.VISIBLE + it.leaderboardList.visibility = View.VISIBLE + } + + /** + * to show progressbar + */ + private fun showProgressBar() = binding?.let { + it.progressBar.visibility = View.VISIBLE + it.scroll.visibility = View.INVISIBLE + } + + /** + * used to hide the layouts while fetching results from api + */ + private fun hideLayouts() = binding?.let { + it.categorySpinner.visibility = View.INVISIBLE + it.durationSpinner.visibility = View.INVISIBLE + it.leaderboardList.visibility = View.INVISIBLE + } + + /** + * check to ensure that user is logged in + */ + private fun checkAccount() = if (sessionManager.currentAccount == null) { + Timber.d("Current account is null") + showLongToast(requireActivity(), resources.getString(R.string.user_not_logged_in)) + sessionManager.forceLogin(requireActivity()) + false + } else { + true + } + + /** + * Shows a generic error toast when error occurs while loading leaderboard + */ + private fun onError() { + showLongToast(requireActivity(), resources.getString(R.string.error_occurred)) + binding?.let { it.progressBar.visibility = View.GONE } + } + + override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + binding = null + } + + private class SelectionListener(private val handler: () -> Unit): AdapterView.OnItemSelectedListener { + override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) = + handler() + + override fun onNothingSelected(p0: AdapterView<*>?) = Unit + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java deleted file mode 100644 index 5558f3d9e..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java +++ /dev/null @@ -1,137 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.DiffUtil.ItemCallback; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * This class represents the leaderboard API response sub part of i.e. leaderboard list - * The leaderboard list will contain the ranking of the users from 1 to n, - * avatars, username and count in the selected category. - */ -public class LeaderboardList { - - /** - * Username of the user - * Example value - Syced - */ - @SerializedName("username") - @Expose - private String username; - - /** - * Count in the category - * Example value - 10 - */ - @SerializedName("category_count") - @Expose - private Integer categoryCount; - - /** - * URL of the avatar of user - * Example value = https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png - */ - @SerializedName("avatar") - @Expose - private String avatar; - - /** - * Rank of the user - * Example value - 1 - */ - @SerializedName("rank") - @Expose - private Integer rank; - - /** - * @return the username of the user in the leaderboard list - */ - public String getUsername() { - return username; - } - - /** - * Sets the username of the user in the leaderboard list - */ - public void setUsername(String username) { - this.username = username; - } - - /** - * @return the category count of the user in the leaderboard list - */ - public Integer getCategoryCount() { - return categoryCount; - } - - /** - * Sets the category count of the user in the leaderboard list - */ - public void setCategoryCount(Integer categoryCount) { - this.categoryCount = categoryCount; - } - - /** - * @return the avatar of the user in the leaderboard list - */ - public String getAvatar() { - return avatar; - } - - /** - * Sets the avatar of the user in the leaderboard list - */ - public void setAvatar(String avatar) { - this.avatar = avatar; - } - - /** - * @return the rank of the user in the leaderboard list - */ - public Integer getRank() { - return rank; - } - - /** - * Sets the rank of the user in the leaderboard list - */ - public void setRank(Integer rank) { - this.rank = rank; - } - - - /** - * This method checks for the diff in the callbacks for paged lists - */ - public static DiffUtil.ItemCallback DIFF_CALLBACK = - new ItemCallback() { - @Override - public boolean areItemsTheSame(@NonNull LeaderboardList oldItem, - @NonNull LeaderboardList newItem) { - return newItem == oldItem; - } - - @Override - public boolean areContentsTheSame(@NonNull LeaderboardList oldItem, - @NonNull LeaderboardList newItem) { - return newItem.getRank().equals(oldItem.getRank()); - } - }; - - /** - * Returns true if two objects are equal, false otherwise - * @param obj - * @return - */ - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - LeaderboardList leaderboardList = (LeaderboardList) obj; - return leaderboardList.getRank().equals(this.getRank()); - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.kt new file mode 100644 index 000000000..dc6d93e15 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.kt @@ -0,0 +1,61 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.recyclerview.widget.DiffUtil +import com.google.gson.annotations.SerializedName + +/** + * This class represents the leaderboard API response sub part of i.e. leaderboard list + * The leaderboard list will contain the ranking of the users from 1 to n, + * avatars, username and count in the selected category. + */ +data class LeaderboardList ( + @SerializedName("username") + var username: String? = null, + @SerializedName("category_count") + var categoryCount: Int? = null, + @SerializedName("avatar") + var avatar: String? = null, + @SerializedName("rank") + var rank: Int? = null +) { + + /** + * Returns true if two objects are equal, false otherwise + * @param other + * @return + */ + override fun equals(other: Any?): Boolean { + if (other === this) { + return true + } + + val leaderboardList = other as LeaderboardList + return leaderboardList.rank == rank + } + + override fun hashCode(): Int { + var result = username?.hashCode() ?: 0 + result = 31 * result + (categoryCount ?: 0) + result = 31 * result + (avatar?.hashCode() ?: 0) + result = 31 * result + (rank ?: 0) + return result + } + + companion object { + /** + * This method checks for the diff in the callbacks for paged lists + */ + var DIFF_CALLBACK: DiffUtil.ItemCallback = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: LeaderboardList, + newItem: LeaderboardList + ): Boolean = newItem === oldItem + + override fun areContentsTheSame( + oldItem: LeaderboardList, + newItem: LeaderboardList + ): Boolean = newItem.rank == oldItem.rank + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java deleted file mode 100644 index 9af24159a..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java +++ /dev/null @@ -1,93 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - - -import android.app.Activity; -import android.content.Context; -import android.net.Uri; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.paging.PagedListAdapter; -import androidx.recyclerview.widget.RecyclerView; -import com.facebook.drawee.view.SimpleDraweeView; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.profile.ProfileActivity; - -/** - * This class extends RecyclerView.Adapter and creates the List section of the leaderboard - */ -public class LeaderboardListAdapter extends PagedListAdapter { - - public LeaderboardListAdapter() { - super(LeaderboardList.DIFF_CALLBACK); - } - - public class ListViewHolder extends RecyclerView.ViewHolder { - TextView rank; - SimpleDraweeView avatar; - TextView username; - TextView count; - - public ListViewHolder(View itemView) { - super(itemView); - this.rank = itemView.findViewById(R.id.user_rank); - this.avatar = itemView.findViewById(R.id.user_avatar); - this.username = itemView.findViewById(R.id.user_name); - this.count = itemView.findViewById(R.id.user_count); - } - - /** - * This method will return the Context - * @return Context - */ - public Context getContext() { - return itemView.getContext(); - } - } - - /** - * Overrides the onCreateViewHolder and inflates the recyclerview list item layout - * @param parent - * @param viewType - * @return - */ - @NonNull - @Override - public LeaderboardListAdapter.ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.leaderboard_list_element, parent, false); - - return new ListViewHolder(view); - } - - /** - * Overrides the onBindViewHolder Set the view at the specific position with the specific value - * @param holder - * @param position - */ - @Override - public void onBindViewHolder(@NonNull LeaderboardListAdapter.ListViewHolder holder, int position) { - TextView rank = holder.rank; - SimpleDraweeView avatar = holder.avatar; - TextView username = holder.username; - TextView count = holder.count; - - rank.setText(getItem(position).getRank().toString()); - - avatar.setImageURI(Uri.parse(getItem(position).getAvatar())); - username.setText(getItem(position).getUsername()); - count.setText(getItem(position).getCategoryCount().toString()); - - /* - Now that we have our in app profile-section, lets take the user there - */ - holder.itemView.setOnClickListener(view -> { - if (view.getContext() instanceof ProfileActivity) { - ((Activity) (view.getContext())).finish(); - } - ProfileActivity.startYourself(view.getContext(), getItem(position).getUsername(), true); - }); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt new file mode 100644 index 000000000..c7bccf950 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.app.Activity +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.facebook.drawee.view.SimpleDraweeView +import fr.free.nrw.commons.R +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.profile.leaderboard.LeaderboardList.Companion.DIFF_CALLBACK +import fr.free.nrw.commons.profile.leaderboard.LeaderboardListAdapter.ListViewHolder + + +/** + * This class extends RecyclerView.Adapter and creates the List section of the leaderboard + */ +class LeaderboardListAdapter : PagedListAdapter(DIFF_CALLBACK) { + inner class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var rank: TextView? = itemView.findViewById(R.id.user_rank) + var avatar: SimpleDraweeView? = itemView.findViewById(R.id.user_avatar) + var username: TextView? = itemView.findViewById(R.id.user_name) + var count: TextView? = itemView.findViewById(R.id.user_count) + } + + /** + * Overrides the onCreateViewHolder and inflates the recyclerview list item layout + * @param parent + * @param viewType + * @return + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder = + ListViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.leaderboard_list_element, parent, false) + ) + + /** + * Overrides the onBindViewHolder Set the view at the specific position with the specific value + * @param holder + * @param position + */ + override fun onBindViewHolder(holder: ListViewHolder, position: Int) = with (holder) { + val item = getItem(position)!! + + rank?.text = item.rank.toString() + avatar?.setImageURI(Uri.parse(item.avatar)) + username?.text = item.username + count?.text = item.categoryCount.toString() + + /* + Now that we have our in app profile-section, lets take the user there + */ + itemView.setOnClickListener { view: View -> + if (view.context is ProfileActivity) { + ((view.context) as Activity).finish() + } + ProfileActivity.startYourself(view.context, item.username, true) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java deleted file mode 100644 index 909b4f646..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java +++ /dev/null @@ -1,107 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Transformations; -import androidx.lifecycle.ViewModel; -import androidx.paging.LivePagedListBuilder; -import androidx.paging.PagedList; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.disposables.CompositeDisposable; - -/** - * Extends the ViewModel class and creates the LeaderboardList View Model - */ -public class LeaderboardListViewModel extends ViewModel { - - private DataSourceFactory dataSourceFactory; - private LiveData> listLiveData; - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - private LiveData progressLoadStatus = new MutableLiveData<>(); - - /** - * Constructor for a new LeaderboardListViewModel - * @param okHttpJsonApiClient - * @param sessionManager - */ - public LeaderboardListViewModel(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager - sessionManager) { - - dataSourceFactory = new DataSourceFactory(okHttpJsonApiClient, - compositeDisposable, sessionManager); - initializePaging(); - } - - - /** - * Initialises the paging - */ - private void initializePaging() { - - PagedList.Config pagedListConfig = - new PagedList.Config.Builder() - .setEnablePlaceholders(false) - .setInitialLoadSizeHint(PAGE_SIZE) - .setPageSize(PAGE_SIZE).build(); - - listLiveData = new LivePagedListBuilder<>(dataSourceFactory, pagedListConfig) - .build(); - - progressLoadStatus = Transformations - .switchMap(dataSourceFactory.getMutableLiveData(), DataSourceClass::getProgressLiveStatus); - - } - - /** - * Refreshes the paged list with the new params and starts the loading of new data - * @param duration - * @param category - * @param limit - * @param offset - */ - public void refresh(String duration, String category, int limit, int offset) { - dataSourceFactory.setDuration(duration); - dataSourceFactory.setCategory(category); - dataSourceFactory.setLimit(limit); - dataSourceFactory.setOffset(offset); - dataSourceFactory.getMutableLiveData().getValue().invalidate(); - } - - /** - * Sets the new params for the paged list API calls - * @param duration - * @param category - * @param limit - * @param offset - */ - public void setParams(String duration, String category, int limit, int offset) { - dataSourceFactory.setDuration(duration); - dataSourceFactory.setCategory(category); - dataSourceFactory.setLimit(limit); - dataSourceFactory.setOffset(offset); - } - - /** - * @return the loading status of paged list - */ - public LiveData getProgressLoadStatus() { - return progressLoadStatus; - } - - /** - * @return the paged list with live data - */ - public LiveData> getListLiveData() { - return listLiveData; - } - - @Override - protected void onCleared() { - super.onCleared(); - compositeDisposable.clear(); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.kt new file mode 100644 index 000000000..7d649b67b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.kt @@ -0,0 +1,54 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.map +import androidx.lifecycle.switchMap +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE + +/** + * Extends the ViewModel class and creates the LeaderboardList View Model + */ +class LeaderboardListViewModel( + okHttpJsonApiClient: OkHttpJsonApiClient, + sessionManager: SessionManager +) : ViewModel() { + private val dataSourceFactory = DataSourceFactory(okHttpJsonApiClient, sessionManager) + + val listLiveData: LiveData> = LivePagedListBuilder( + dataSourceFactory, + PagedList.Config.Builder() + .setEnablePlaceholders(false) + .setInitialLoadSizeHint(PAGE_SIZE) + .setPageSize(PAGE_SIZE).build() + ).build() + + val progressLoadStatus: LiveData = + dataSourceFactory.mutableLiveData.switchMap { it.progressLiveStatus } + + /** + * Refreshes the paged list with the new params and starts the loading of new data + */ + fun refresh(duration: String?, category: String?, limit: Int, offset: Int) { + dataSourceFactory.duration = duration + dataSourceFactory.category = category + dataSourceFactory.limit = limit + dataSourceFactory.offset = offset + dataSourceFactory.mutableLiveData.value!!.invalidate() + } + + /** + * Sets the new params for the paged list API calls + */ + fun setParams(duration: String?, category: String?, limit: Int, offset: Int) { + dataSourceFactory.duration = duration + dataSourceFactory.category = category + dataSourceFactory.limit = limit + dataSourceFactory.offset = offset + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java deleted file mode 100644 index 34294fca9..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java +++ /dev/null @@ -1,237 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import java.util.List; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * GSON Response Class for Leaderboard API response - */ -public class LeaderboardResponse { - - /** - * Status Code returned from the API - * Example value - 200 - */ - @SerializedName("status") - @Expose - private Integer status; - - /** - * Username returned from the API - * Example value - Syced - */ - @SerializedName("username") - @Expose - private String username; - - /** - * Category count returned from the API - * Example value - 10 - */ - @SerializedName("category_count") - @Expose - private Integer categoryCount; - - /** - * Limit returned from the API - * Example value - 10 - */ - @SerializedName("limit") - @Expose - private int limit; - - /** - * Avatar returned from the API - * Example value - https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png - */ - @SerializedName("avatar") - @Expose - private String avatar; - - /** - * Offset returned from the API - * Example value - 0 - */ - @SerializedName("offset") - @Expose - private int offset; - - /** - * Duration returned from the API - * Example value - yearly - */ - @SerializedName("duration") - @Expose - private String duration; - - /** - * Leaderboard list returned from the API - * Example value - [{ - * "username": "Fæ", - * "category_count": 107147, - * "avatar": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png", - * "rank": 1 - * }] - */ - @SerializedName("leaderboard_list") - @Expose - private List leaderboardList = null; - - /** - * Category returned from the API - * Example value - upload - */ - @SerializedName("category") - @Expose - private String category; - - /** - * Rank returned from the API - * Example value - 1 - */ - @SerializedName("rank") - @Expose - private Integer rank; - - /** - * @return the status code - */ - public Integer getStatus() { - return status; - } - - /** - * Sets the status code - */ - public void setStatus(Integer status) { - this.status = status; - } - - /** - * @return the username - */ - public String getUsername() { - return username; - } - - /** - * Sets the username - */ - public void setUsername(String username) { - this.username = username; - } - - /** - * @return the category count - */ - public Integer getCategoryCount() { - return categoryCount; - } - - /** - * Sets the category count - */ - public void setCategoryCount(Integer categoryCount) { - this.categoryCount = categoryCount; - } - - /** - * @return the limit - */ - public int getLimit() { - return limit; - } - - /** - * Sets the limit - */ - public void setLimit(int limit) { - this.limit = limit; - } - - /** - * @return the avatar - */ - public String getAvatar() { - return avatar; - } - - /** - * Sets the avatar - */ - public void setAvatar(String avatar) { - this.avatar = avatar; - } - - /** - * @return the offset - */ - public int getOffset() { - return offset; - } - - /** - * Sets the offset - */ - public void setOffset(int offset) { - this.offset = offset; - } - - /** - * @return the duration - */ - public String getDuration() { - return duration; - } - - /** - * Sets the duration - */ - public void setDuration(String duration) { - this.duration = duration; - } - - /** - * @return the leaderboard list - */ - public List getLeaderboardList() { - return leaderboardList; - } - - /** - * Sets the leaderboard list - */ - public void setLeaderboardList(List leaderboardList) { - this.leaderboardList = leaderboardList; - } - - /** - * @return the category - */ - public String getCategory() { - return category; - } - - /** - * Sets the category - */ - public void setCategory(String category) { - this.category = category; - } - - /** - * @return the rank - */ - public Integer getRank() { - return rank; - } - - /** - * Sets the rank - */ - public void setRank(Integer rank) { - this.rank = rank; - } - -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.kt new file mode 100644 index 000000000..8be342650 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.kt @@ -0,0 +1,19 @@ +package fr.free.nrw.commons.profile.leaderboard + +import com.google.gson.annotations.SerializedName + +/** + * GSON Response Class for Leaderboard API response + */ +data class LeaderboardResponse( + @SerializedName("status") var status: Int? = null, + @SerializedName("username") var username: String? = null, + @SerializedName("category_count") var categoryCount: Int? = null, + @SerializedName("limit") var limit: Int = 0, + @SerializedName("avatar") var avatar: String? = null, + @SerializedName("offset") var offset: Int = 0, + @SerializedName("duration") var duration: String? = null, + @SerializedName("leaderboard_list") var leaderboardList: List? = null, + @SerializedName("category") var category: String? = null, + @SerializedName("rank") var rank: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java deleted file mode 100644 index 15449a488..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java +++ /dev/null @@ -1,77 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * GSON Response Class for Update Avatar API response - */ -public class UpdateAvatarResponse { - - /** - * Status Code returned from the API - * Example value - 200 - */ - @SerializedName("status") - @Expose - private String status; - - /** - * Message returned from the API - * Example value - Avatar Updated - */ - @SerializedName("message") - @Expose - private String message; - - /** - * Username returned from the API - * Example value - Syced - */ - @SerializedName("user") - @Expose - private String user; - - /** - * @return the status code - */ - public String getStatus() { - return status; - } - - /** - * Sets the status code - */ - public void setStatus(String status) { - this.status = status; - } - - /** - * @return the message - */ - public String getMessage() { - return message; - } - - /** - * Sets the message - */ - public void setMessage(String message) { - this.message = message; - } - - /** - * @return the username - */ - public String getUser() { - return user; - } - - /** - * Sets the username - */ - public void setUser(String user) { - this.user = user; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.kt new file mode 100644 index 000000000..75fb8f268 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.kt @@ -0,0 +1,10 @@ +package fr.free.nrw.commons.profile.leaderboard + +/** + * GSON Response Class for Update Avatar API response + */ +data class UpdateAvatarResponse( + var status: String? = null, + var message: String? = null, + var user: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java deleted file mode 100644 index 75b9de938..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java +++ /dev/null @@ -1,126 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; -import android.net.Uri; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import com.facebook.drawee.view.SimpleDraweeView; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; - - -/** - * This class extends RecyclerView.Adapter and creates the UserDetail section of the leaderboard - */ -public class UserDetailAdapter extends RecyclerView.Adapter { - - private LeaderboardResponse leaderboardResponse; - - /** - * Stores the username of currently logged in user. - */ - private String currentlyLoggedInUserName = null; - - public UserDetailAdapter(LeaderboardResponse leaderboardResponse) { - this.leaderboardResponse = leaderboardResponse; - } - - public class DataViewHolder extends RecyclerView.ViewHolder { - - private TextView rank; - private SimpleDraweeView avatar; - private TextView username; - private TextView count; - - public DataViewHolder(@NonNull View itemView) { - super(itemView); - this.rank = itemView.findViewById(R.id.rank); - this.avatar = itemView.findViewById(R.id.avatar); - this.username = itemView.findViewById(R.id.username); - this.count = itemView.findViewById(R.id.count); - } - - /** - * This method will return the Context - * @return Context - */ - public Context getContext() { - return itemView.getContext(); - } - } - - /** - * Overrides the onCreateViewHolder and sets the view with leaderboard user element layout - * @param parent - * @param viewType - * @return - */ - @NonNull - @Override - public UserDetailAdapter.DataViewHolder onCreateViewHolder(@NonNull ViewGroup parent, - int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.leaderboard_user_element, parent, false); - return new DataViewHolder(view); - } - - /** - * Overrides the onBindViewHolder Set the view at the specific position with the specific value - * @param holder - * @param position - */ - @Override - public void onBindViewHolder(@NonNull UserDetailAdapter.DataViewHolder holder, int position) { - TextView rank = holder.rank; - SimpleDraweeView avatar = holder.avatar; - TextView username = holder.username; - TextView count = holder.count; - - rank.setText(String.format("%s %d", - holder.getContext().getResources().getString(R.string.rank_prefix), - leaderboardResponse.getRank())); - - avatar.setImageURI( - Uri.parse(leaderboardResponse.getAvatar())); - username.setText(leaderboardResponse.getUsername()); - count.setText(String.format("%s %d", - holder.getContext().getResources().getString(R.string.count_prefix), - leaderboardResponse.getCategoryCount())); - - // When user tap on avatar shows the toast on how to change avatar - // fixing: https://github.com/commons-app/apps-android-commons/issues/47747 - if (currentlyLoggedInUserName == null) { - // If the current login username has not been fetched yet, then fetch it. - final AccountManager accountManager = AccountManager.get(username.getContext()); - final Account[] allAccounts = accountManager.getAccountsByType( - BuildConfig.ACCOUNT_TYPE); - if (allAccounts.length != 0) { - currentlyLoggedInUserName = allAccounts[0].name; - } - } - if (currentlyLoggedInUserName != null && currentlyLoggedInUserName.equals( - leaderboardResponse.getUsername())) { - - avatar.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Toast.makeText(v.getContext(), - R.string.set_up_avatar_toast_string, - Toast.LENGTH_LONG).show(); - } - }); - } - } - - @Override - public int getItemCount() { - return 1; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt new file mode 100644 index 000000000..34fd5ab58 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt @@ -0,0 +1,91 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.accounts.AccountManager +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.recyclerview.widget.RecyclerView +import com.facebook.drawee.view.SimpleDraweeView +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.R +import fr.free.nrw.commons.profile.leaderboard.UserDetailAdapter.DataViewHolder +import java.util.Locale + +/** + * This class extends RecyclerView.Adapter and creates the UserDetail section of the leaderboard + */ +class UserDetailAdapter(private val leaderboardResponse: LeaderboardResponse) : + RecyclerView.Adapter() { + /** + * Stores the username of currently logged in user. + */ + private var currentlyLoggedInUserName: String? = null + + class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val rank: TextView = itemView.findViewById(R.id.rank) + val avatar: SimpleDraweeView = itemView.findViewById(R.id.avatar) + val username: TextView = itemView.findViewById(R.id.username) + val count: TextView = itemView.findViewById(R.id.count) + } + + /** + * Overrides the onCreateViewHolder and sets the view with leaderboard user element layout + * @param parent + * @param viewType + * @return + */ + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): DataViewHolder = DataViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.leaderboard_user_element, parent, false) + ) + + /** + * Overrides the onBindViewHolder Set the view at the specific position with the specific value + * @param holder + * @param position + */ + override fun onBindViewHolder(holder: DataViewHolder, position: Int) = with(holder) { + val resources = itemView.context.resources + + avatar.setImageURI(Uri.parse(leaderboardResponse.avatar)) + username.text = leaderboardResponse.username + rank.text = String.format( + Locale.getDefault(), + "%s %d", + resources.getString(R.string.rank_prefix), + leaderboardResponse.rank + ) + count.text = String.format( + Locale.getDefault(), + "%s %d", + resources.getString(R.string.count_prefix), + leaderboardResponse.categoryCount + ) + + // When user tap on avatar shows the toast on how to change avatar + // fixing: https://github.com/commons-app/apps-android-commons/issues/47747 + if (currentlyLoggedInUserName == null) { + // If the current login username has not been fetched yet, then fetch it. + val accountManager = AccountManager.get(itemView.context) + val allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE) + if (allAccounts.isNotEmpty()) { + currentlyLoggedInUserName = allAccounts[0].name + } + } + if (currentlyLoggedInUserName != null && currentlyLoggedInUserName == leaderboardResponse.username) { + avatar.setOnClickListener { v: View -> + Toast.makeText( + v.context, R.string.set_up_avatar_toast_string, Toast.LENGTH_LONG + ).show() + } + } + } + + override fun getItemCount(): Int = 1 +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java deleted file mode 100644 index fece77110..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import androidx.annotation.NonNull; -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import javax.inject.Inject; - -/** - * This class extends the ViewModelProvider.Factory and creates a ViewModelFactory class - * for leaderboardListViewModel - */ -public class ViewModelFactory implements ViewModelProvider.Factory { - - private OkHttpJsonApiClient okHttpJsonApiClient; - private SessionManager sessionManager; - - - @Inject - public ViewModelFactory(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager sessionManager) { - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.sessionManager = sessionManager; - } - - - /** - * Creats a new LeaderboardListViewModel - * @param modelClass - * @param - * @return - */ - @NonNull - @Override - public T create(@NonNull Class modelClass) { - if (modelClass.isAssignableFrom(LeaderboardListViewModel.class)) { - return (T) new LeaderboardListViewModel(okHttpJsonApiClient, sessionManager); - } - throw new IllegalArgumentException("Unknown class name"); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.kt new file mode 100644 index 000000000..f325355e0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.kt @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import javax.inject.Inject + + +/** + * This class extends the ViewModelProvider.Factory and creates a ViewModelFactory class + * for leaderboardListViewModel + */ +class ViewModelFactory @Inject constructor( + private val okHttpJsonApiClient: OkHttpJsonApiClient, + private val sessionManager: SessionManager +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + if (modelClass.isAssignableFrom(LeaderboardListViewModel::class.java)) { + LeaderboardListViewModel(okHttpJsonApiClient, sessionManager) as T + } else { + throw IllegalArgumentException("Unknown class name") + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java deleted file mode 100644 index 51d806a88..000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java +++ /dev/null @@ -1,116 +0,0 @@ -package fr.free.nrw.commons.leaderboard; - -import com.google.gson.Gson; -import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Request.Builder; -import okhttp3.Response; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -/** - * This class tests the Leaderboard API calls - */ -public class LeaderboardApiTest { - - MockWebServer server; - private static final String TEST_USERNAME = "user"; - private static final String TEST_AVATAR = "avatar"; - private static final int TEST_USER_RANK = 1; - private static final int TEST_USER_COUNT = 0; - - private static final String FILE_NAME = "leaderboard_sample_response.json"; - private static final String ENDPOINT = "/leaderboard.py"; - - /** - * This method initialises a Mock Server - */ - @Before - public void initTest() { - server = new MockWebServer(); - } - - /** - * This method will setup a Mock Server and load Test JSON Response File - * @throws Exception - */ - @Before - public void setUp() throws Exception { - - String testResponseBody = convertStreamToString(getClass().getClassLoader().getResourceAsStream(FILE_NAME)); - - server.enqueue(new MockResponse().setBody(testResponseBody)); - server.start(); - } - - /** - * This method converts a Input Stream to String - * @param is takes Input Stream of JSON File as Parameter - * @return a String with JSON data - * @throws Exception - */ - private static String convertStreamToString(InputStream is) throws Exception { - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - } - reader.close(); - return sb.toString(); - } - - /** - * This method will call the Mock Server and Test it with sample values. - * It will test the Leaderboard API call functionality and check if the object is - * being created with the correct values - * @throws IOException - */ - @Test - public void apiTest() throws IOException { - HttpUrl httpUrl = server.url(ENDPOINT); - LeaderboardResponse response = sendRequest(new OkHttpClient(), httpUrl); - - Assert.assertEquals(TEST_AVATAR, response.getAvatar()); - Assert.assertEquals(TEST_USERNAME, response.getUsername()); - Assert.assertEquals(Integer.valueOf(TEST_USER_RANK), response.getRank()); - Assert.assertEquals(Integer.valueOf(TEST_USER_COUNT), response.getCategoryCount()); - } - - /** - * This method will call the Mock API and returns the Leaderboard Response Object - * @param okHttpClient - * @param httpUrl - * @return Leaderboard Response Object - * @throws IOException - */ - private LeaderboardResponse sendRequest(OkHttpClient okHttpClient, HttpUrl httpUrl) - throws IOException { - Request request = new Builder().url(httpUrl).build(); - Response response = okHttpClient.newCall(request).execute(); - if (response.isSuccessful()) { - Gson gson = new Gson(); - return gson.fromJson(response.body().string(), LeaderboardResponse.class); - } - return null; - } - - /** - * This method shuts down the Mock Server - * @throws IOException - */ - @After - public void shutdown() throws IOException { - server.shutdown(); - } -} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.kt new file mode 100644 index 000000000..ac0da42f3 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.kt @@ -0,0 +1,121 @@ +package fr.free.nrw.commons.leaderboard + +import com.google.gson.Gson +import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader + +/** + * This class tests the Leaderboard API calls + */ +class LeaderboardApiTest { + lateinit var server: MockWebServer + + /** + * This method initialises a Mock Server + */ + @Before + fun initTest() { + server = MockWebServer() + } + + /** + * This method will setup a Mock Server and load Test JSON Response File + * @throws Exception + */ + @Before + @Throws(Exception::class) + fun setUp() { + val testResponseBody = convertStreamToString( + javaClass.classLoader!!.getResourceAsStream(FILE_NAME) + ) + + server.enqueue(MockResponse().setBody(testResponseBody)) + server.start() + } + + /** + * This method will call the Mock Server and Test it with sample values. + * It will test the Leaderboard API call functionality and check if the object is + * being created with the correct values + * @throws IOException + */ + @Test + @Throws(IOException::class) + fun apiTest() { + val httpUrl = server.url(ENDPOINT) + val response = sendRequest(OkHttpClient(), httpUrl) + + Assert.assertEquals(TEST_AVATAR, response!!.avatar) + Assert.assertEquals(TEST_USERNAME, response.username) + Assert.assertEquals(TEST_USER_RANK, response.rank) + Assert.assertEquals(TEST_USER_COUNT, response.categoryCount) + } + + /** + * This method will call the Mock API and returns the Leaderboard Response Object + * @param okHttpClient + * @param httpUrl + * @return Leaderboard Response Object + * @throws IOException + */ + @Throws(IOException::class) + private fun sendRequest(okHttpClient: OkHttpClient, httpUrl: HttpUrl): LeaderboardResponse? { + val request: Request = Request.Builder().url(httpUrl).build() + val response = okHttpClient.newCall(request).execute() + if (response.isSuccessful) { + val gson = Gson() + return gson.fromJson(response.body!!.string(), LeaderboardResponse::class.java) + } + return null + } + + /** + * This method shuts down the Mock Server + * @throws IOException + */ + @After + @Throws(IOException::class) + fun shutdown() { + server.shutdown() + } + + companion object { + private const val TEST_USERNAME = "user" + private const val TEST_AVATAR = "avatar" + private const val TEST_USER_RANK = 1 + private const val TEST_USER_COUNT = 0 + + private const val FILE_NAME = "leaderboard_sample_response.json" + private const val ENDPOINT = "/leaderboard.py" + + /** + * This method converts a Input Stream to String + * @param is takes Input Stream of JSON File as Parameter + * @return a String with JSON data + * @throws Exception + */ + @Throws(Exception::class) + private fun convertStreamToString(`is`: InputStream): String { + val reader = BufferedReader(InputStreamReader(`is`)) + val sb = StringBuilder() + var line: String? + while ((reader.readLine().also { line = it }) != null) { + sb.append(line).append("\n") + } + reader.close() + return sb.toString() + } + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java deleted file mode 100644 index 7c2b25d3b..000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package fr.free.nrw.commons.leaderboard; - -import com.google.gson.Gson; -import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Request.Builder; -import okhttp3.Response; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -public class UpdateAvatarApiTest { - - private static final String TEST_USERNAME = "user"; - private static final String TEST_STATUS = "200"; - private static final String TEST_MESSAGE = "Avatar Updated"; - private static final String FILE_NAME = "update_leaderboard_avatar_sample_response.json"; - private static final String ENDPOINT = "/update_avatar.py"; - MockWebServer server; - - /** - * This method converts a Input Stream to String - * - * @param is takes Input Stream of JSON File as Parameter - * @return a String with JSON data - * @throws Exception - */ - private static String convertStreamToString(final InputStream is) throws Exception { - final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - final StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - } - reader.close(); - return sb.toString(); - } - - /** - * This method initialises a Mock Server - */ - @Before - public void initTest() { - server = new MockWebServer(); - } - - /** - * This method will setup a Mock Server and load Test JSON Response File - * - * @throws Exception - */ - @Before - public void setUp() throws Exception { - - final String testResponseBody = convertStreamToString( - getClass().getClassLoader().getResourceAsStream(FILE_NAME)); - - server.enqueue(new MockResponse().setBody(testResponseBody)); - server.start(); - } - - /** - * This method will call the Mock Server and Test it with sample values. It will test the Update - * Avatar API call functionality and check if the object is being created with the correct - * values - * - * @throws IOException - */ - @Test - public void apiTest() throws IOException { - final HttpUrl httpUrl = server.url(ENDPOINT); - final UpdateAvatarResponse response = sendRequest(new OkHttpClient(), httpUrl); - - Assert.assertEquals(TEST_USERNAME, response.getUser()); - Assert.assertEquals(TEST_STATUS, response.getStatus()); - Assert.assertEquals(TEST_MESSAGE, response.getMessage()); - } - - /** - * This method will call the Mock API and returns the Update Avatar Response Object - * - * @param okHttpClient - * @param httpUrl - * @return Update Avatar Response Object - * @throws IOException - */ - private UpdateAvatarResponse sendRequest(final OkHttpClient okHttpClient, final HttpUrl httpUrl) - throws IOException { - final Request request = new Builder().url(httpUrl).build(); - final Response response = okHttpClient.newCall(request).execute(); - if (response.isSuccessful()) { - final Gson gson = new Gson(); - return gson.fromJson(response.body().string(), UpdateAvatarResponse.class); - } - return null; - } - - /** - * This method shuts down the Mock Server - * - * @throws IOException - */ - @After - public void shutdown() throws IOException { - server.shutdown(); - } -} - diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.kt new file mode 100644 index 000000000..6b7f064cf --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.kt @@ -0,0 +1,127 @@ +package fr.free.nrw.commons.leaderboard + +import com.google.gson.Gson +import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader + +class UpdateAvatarApiTest { + lateinit var server: MockWebServer + + /** + * This method initialises a Mock Server + */ + @Before + fun initTest() { + server = MockWebServer() + } + + /** + * This method will setup a Mock Server and load Test JSON Response File + * + * @throws Exception + */ + @Before + @Throws(Exception::class) + fun setUp() { + val testResponseBody = convertStreamToString( + javaClass.classLoader!!.getResourceAsStream(FILE_NAME) + ) + + server.enqueue(MockResponse().setBody(testResponseBody)) + server.start() + } + + /** + * This method will call the Mock Server and Test it with sample values. It will test the Update + * Avatar API call functionality and check if the object is being created with the correct + * values + * + * @throws IOException + */ + @Test + @Throws(IOException::class) + fun apiTest() { + val httpUrl = server.url(ENDPOINT) + val response = sendRequest(OkHttpClient(), httpUrl) + Assert.assertNotNull(response) + + with(response!!) { + Assert.assertEquals(TEST_USERNAME, user) + Assert.assertEquals(TEST_STATUS, status) + Assert.assertEquals(TEST_MESSAGE, message) + } + } + + /** + * This method will call the Mock API and returns the Update Avatar Response Object + * + * @param okHttpClient + * @param httpUrl + * @return Update Avatar Response Object + * @throws IOException + */ + @Throws(IOException::class) + private fun sendRequest(okHttpClient: OkHttpClient, httpUrl: HttpUrl): UpdateAvatarResponse? { + val request: Request = Request.Builder().url(httpUrl).build() + val response = okHttpClient.newCall(request).execute() + if (response.isSuccessful) { + val gson = Gson() + return gson.fromJson( + response.body!!.string(), + UpdateAvatarResponse::class.java + ) + } + return null + } + + /** + * This method shuts down the Mock Server + * + * @throws IOException + */ + @After + @Throws(IOException::class) + fun shutdown() { + server.shutdown() + } + + companion object { + private const val TEST_USERNAME = "user" + private const val TEST_STATUS = "200" + private const val TEST_MESSAGE = "Avatar Updated" + private const val FILE_NAME = "update_leaderboard_avatar_sample_response.json" + private const val ENDPOINT = "/update_avatar.py" + + /** + * This method converts a Input Stream to String + * + * @param is takes Input Stream of JSON File as Parameter + * @return a String with JSON data + * @throws Exception + */ + @Throws(Exception::class) + private fun convertStreamToString(`is`: InputStream): String { + val reader = BufferedReader(InputStreamReader(`is`)) + val sb = StringBuilder() + var line: String? + while ((reader.readLine().also { line = it }) != null) { + sb.append(line).append("\n") + } + reader.close() + return sb.toString() + } + } +} +