Convert profile package to kotlin (#5979)

* Convert ViewModelFactory to kotlin

* Convert UpdateAvatarResponse and related test to Kotlin

* Convert LeaderboardResponse and related test to kotlin

* Convert LeaderboardListAdapter to kotlin

* Convert UserDetailAdapter to kotlin

* Convert LeaderboardListViewModel to kotlin

* Convert DataSourceClass to kotlin

* Convert the LeaderboardFragment to kotlin

* Converted AchievementsFragment to kotlin

* Revert "Converted AchievementsFragment to kotlin"

This reverts commit 4fcbb81e5d.

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
Paul Hawke 2024-12-03 00:47:25 -06:00 committed by GitHub
parent 8265cc6306
commit 33548fa57d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1042 additions and 1694 deletions

View file

@ -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<Integer, LeaderboardList> {
private OkHttpJsonApiClient okHttpJsonApiClient;
private SessionManager sessionManager;
private MutableLiveData<String> 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<String> getProgressLiveStatus() {
return progressLiveStatus;
}
/**
* Loads the initial set of data from API
* @param params
* @param callback
*/
@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params,
@NonNull LoadInitialCallback<Integer, LeaderboardList> 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<Integer> params,
@NonNull LoadCallback<Integer, LeaderboardList> 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<Integer> params,
@NonNull LoadCallback<Integer, LeaderboardList> 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);
}
));
}
}

View file

@ -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<Int, LeaderboardList>() {
val progressLiveStatus: MutableLiveData<LeaderboardConstants.LoadingStatus> = MutableLiveData()
private val compositeDisposable = CompositeDisposable()
override fun loadInitial(
params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, LeaderboardList?>
) {
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<Int>, callback: LoadCallback<Int, LeaderboardList?>
) = Unit
override fun loadAfter(
params: LoadParams<Int>, callback: LoadCallback<Int, LeaderboardList?>
) {
compositeDisposable.add(okHttpJsonApiClient.getLeaderboard(
Objects.requireNonNull<Account?>(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)
}))
}
}

View file

@ -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<Integer, LeaderboardList> {
private MutableLiveData<DataSourceClass> 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<DataSourceClass> getMutableLiveData() {
return liveData;
}
/**
* Creates the new instance of data source class
* @return
*/
@Override
public DataSource<Integer, LeaderboardList> create() {
DataSourceClass dataSourceClass = new DataSourceClass(okHttpJsonApiClient, sessionManager, duration, category, limit, offset);
liveData.postValue(dataSourceClass);
return dataSourceClass;
}
}

View file

@ -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<Int, LeaderboardList>() {
val mutableLiveData: MutableLiveData<DataSourceClass> = 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<Int, LeaderboardList> = DataSourceClass(
okHttpJsonApiClient, sessionManager, duration, category, limit, offset
).also { mutableLiveData.postValue(it) }
}

View file

@ -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";
}

View file

@ -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"
}

View file

@ -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<CharSequence> 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<CharSequence> 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;
}
}

View file

@ -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
}
}

View file

@ -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<LeaderboardList> DIFF_CALLBACK =
new ItemCallback<LeaderboardList>() {
@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());
}
}

View file

@ -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<LeaderboardList> =
object : DiffUtil.ItemCallback<LeaderboardList>() {
override fun areItemsTheSame(
oldItem: LeaderboardList,
newItem: LeaderboardList
): Boolean = newItem === oldItem
override fun areContentsTheSame(
oldItem: LeaderboardList,
newItem: LeaderboardList
): Boolean = newItem.rank == oldItem.rank
}
}
}

View file

@ -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<LeaderboardList, LeaderboardListAdapter.ListViewHolder> {
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);
});
}
}

View file

@ -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<LeaderboardList, ListViewHolder>(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)
}
}
}

View file

@ -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<PagedList<LeaderboardList>> listLiveData;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private LiveData<String> 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<String> getProgressLoadStatus() {
return progressLoadStatus;
}
/**
* @return the paged list with live data
*/
public LiveData<PagedList<LeaderboardList>> getListLiveData() {
return listLiveData;
}
@Override
protected void onCleared() {
super.onCleared();
compositeDisposable.clear();
}
}

View file

@ -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<PagedList<LeaderboardList>> = LivePagedListBuilder(
dataSourceFactory,
PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(PAGE_SIZE)
.setPageSize(PAGE_SIZE).build()
).build()
val progressLoadStatus: LiveData<LoadingStatus> =
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
}
}

View file

@ -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": "",
* "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> 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<LeaderboardList> getLeaderboardList() {
return leaderboardList;
}
/**
* Sets the leaderboard list
*/
public void setLeaderboardList(List<LeaderboardList> 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;
}
}

View file

@ -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<LeaderboardList>? = null,
@SerializedName("category") var category: String? = null,
@SerializedName("rank") var rank: Int? = null
)

View file

@ -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;
}
}

View file

@ -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
)

View file

@ -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<UserDetailAdapter.DataViewHolder> {
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;
}
}

View file

@ -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<DataViewHolder>() {
/**
* 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
}

View file

@ -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 <T>
* @return
*/
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (modelClass.isAssignableFrom(LeaderboardListViewModel.class)) {
return (T) new LeaderboardListViewModel(okHttpJsonApiClient, sessionManager);
}
throw new IllegalArgumentException("Unknown class name");
}
}

View file

@ -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 <T : ViewModel> create(modelClass: Class<T>): T =
if (modelClass.isAssignableFrom(LeaderboardListViewModel::class.java)) {
LeaderboardListViewModel(okHttpJsonApiClient, sessionManager) as T
} else {
throw IllegalArgumentException("Unknown class name")
}
}

View file

@ -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();
}
}

View file

@ -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()
}
}
}

View file

@ -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();
}
}

View file

@ -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()
}
}
}