diff --git a/app/build.gradle b/app/build.gradle
index 638a4048a..dd64aa19b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,7 +21,7 @@ dependencies {
// Utils
implementation 'in.yuvi:http.fluent:1.3'
implementation 'com.google.code.gson:gson:2.8.5'
- implementation 'com.squareup.okhttp3:okhttp:4.5.0'
+ implementation 'com.squareup.okhttp3:okhttp:4.8.0'
implementation 'com.squareup.okio:okio:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
@@ -42,6 +42,7 @@ dependencies {
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0'
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:$ADAPTER_DELEGATES_VERSION"
@@ -50,6 +51,7 @@ dependencies {
testImplementation "androidx.paging:paging-common-ktx:$PAGING_VERSION"
implementation "androidx.paging:paging-rxjava2-ktx:$PAGING_VERSION"
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
+ implementation 'com.squareup.okhttp3:okhttp-ws:3.4.1'
// Logging
implementation 'ch.acra:acra-dialog:5.3.0'
@@ -79,7 +81,7 @@ dependencies {
testImplementation 'junit:junit:4.13'
testImplementation 'org.robolectric:robolectric:4.3'
testImplementation 'androidx.test:core:1.2.0'
- testImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1'
+ testImplementation "com.squareup.okhttp3:mockwebserver:4.8.0"
testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5"
testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
testImplementation 'org.mockito:mockito-core:2.23.0'
@@ -94,7 +96,7 @@ dependencies {
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.annotation:annotation:1.1.0'
- androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1'
+ androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.0'
androidTestUtil 'androidx.test:orchestrator:1.2.0'
// Debugging
@@ -209,8 +211,8 @@ android {
configurations.all {
resolutionStrategy.force 'androidx.annotation:annotation:1.0.2'
+ exclude module: 'okhttp-ws'
}
-
flavorDimensions 'tier'
productFlavors {
prod {
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/AchievementsActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/AchievementsActivityTest.kt
index 198578f2f..6bded4351 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/AchievementsActivityTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/AchievementsActivityTest.kt
@@ -7,10 +7,9 @@ import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.espresso.matcher.ViewMatchers.withId
-import androidx.test.filters.MediumTest
import androidx.test.runner.AndroidJUnit4
-import fr.free.nrw.commons.achievements.AchievementsActivity
import fr.free.nrw.commons.auth.LoginActivity
+import fr.free.nrw.commons.profile.ProfileActivity
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -32,6 +31,6 @@ class AchievementsActivityTest {
onView(withId(R.id.drawer_layout)).perform(DrawerActions.open())
onView(withId(R.id.user_icon)).perform(click())
- Intents.intended(hasComponent(AchievementsActivity::class.java.name))
+ Intents.intended(hasComponent(ProfileActivity::class.java.name))
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6f8eade2e..7a0c876de 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -137,8 +137,8 @@
/>
+ android:name=".profile.ProfileActivity"
+ android:label="@string/Profile" />
getLeaderboard(String userName, String duration, String category, String limit, String offset) {
+ final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
+ + LEADERBOARD_END_POINT;
+ String url = String.format(Locale.ENGLISH,
+ fetchLeaderboardUrlTemplate,
+ userName,
+ duration,
+ category,
+ limit,
+ offset);
+ HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
+ urlBuilder.addQueryParameter("user", userName);
+ urlBuilder.addQueryParameter("duration", duration);
+ urlBuilder.addQueryParameter("category", category);
+ urlBuilder.addQueryParameter("limit", limit);
+ urlBuilder.addQueryParameter("offset", offset);
+ Timber.i("Url %s", urlBuilder.toString());
+ Request request = new Request.Builder()
+ .url(urlBuilder.toString())
+ .build();
+ return Observable.fromCallable(() -> {
+ Response response = okHttpClient.newCall(request).execute();
+ if (response != null && response.body() != null && response.isSuccessful()) {
+ String json = response.body().string();
+ if (json == null) {
+ return new LeaderboardResponse();
+ }
+ Timber.d("Response for leaderboard is %s", json);
+ try {
+ return gson.fromJson(json, LeaderboardResponse.class);
+ } catch (Exception e) {
+ return new LeaderboardResponse();
+ }
+ }
+ return new LeaderboardResponse();
+ });
+ }
+
+ /**
+ * This method will update the leaderboard user avatar
+ * @param username username to update
+ * @param avatar url of the new avatar
+ * @return UpdateAvatarResponse object
+ */
+ @NonNull
+ public Single setAvatar(String username, String avatar) {
+ final String urlTemplate = wikiMediaTestToolforgeUrl
+ + UPDATE_AVATAR_END_POINT;
+ return Single.fromCallable(() -> {
+ String url = String.format(Locale.ENGLISH,
+ urlTemplate,
+ username,
+ avatar);
+ HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
+ urlBuilder.addQueryParameter("user", username);
+ urlBuilder.addQueryParameter("avatar", avatar);
+ Timber.i("Url %s", urlBuilder.toString());
+ Request request = new Request.Builder()
+ .url(urlBuilder.toString())
+ .build();
+ Response response = okHttpClient.newCall(request).execute();
+ if (response != null && response.body() != null && response.isSuccessful()) {
+ String json = response.body().string();
+ if (json == null) {
+ return null;
+ }
+ try {
+ return gson.fromJson(json, UpdateAvatarResponse.class);
+ } catch (Exception e) {
+ return new UpdateAvatarResponse();
+ }
+ }
+ return null;
+ });
+ }
+
@NonNull
public Single getUploadCount(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
@@ -145,7 +239,6 @@ public class OkHttpJsonApiClient {
userName);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
- Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java
new file mode 100644
index 000000000..70b1dccd6
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java
@@ -0,0 +1,84 @@
+package fr.free.nrw.commons.profile;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.viewpager.widget.ViewPager;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import com.google.android.material.tabs.TabLayout;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.profile.achievements.AchievementsFragment;
+import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment;
+import fr.free.nrw.commons.theme.NavigationBaseActivity;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This activity will set two tabs, achievements and
+ * each tab will have their own fragments
+ */
+public class ProfileActivity extends NavigationBaseActivity {
+
+ private FragmentManager supportFragmentManager;
+
+ @BindView(R.id.viewPager)
+ ViewPager viewPager;
+
+ @BindView(R.id.tab_layout)
+ TabLayout tabLayout;
+
+ private ViewPagerAdapter viewPagerAdapter;
+ private AchievementsFragment achievementsFragment;
+ private LeaderboardFragment leaderboardFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_profile);
+ ButterKnife.bind(this);
+ initDrawer();
+ setTitle(R.string.Profile);
+
+ supportFragmentManager = getSupportFragmentManager();
+ viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
+ viewPager.setAdapter(viewPagerAdapter);
+ tabLayout.setupWithViewPager(viewPager);
+ setTabs();
+ }
+
+ /**
+ * Creates a way to change current activity to AchievementActivity
+ * @param context
+ */
+ public static void startYourself(Context context) {
+ Intent intent = new Intent(context, ProfileActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ context.startActivity(intent);
+ }
+
+ /**
+ * Set the tabs for the fragments
+ */
+ private void setTabs() {
+ List fragmentList = new ArrayList<>();
+ List titleList = new ArrayList<>();
+ achievementsFragment = new AchievementsFragment();
+ fragmentList.add(achievementsFragment);
+ titleList.add(getResources().getString(R.string.achievements_tab_title).toUpperCase());
+ leaderboardFragment = new LeaderboardFragment();
+ fragmentList.add(leaderboardFragment);
+ titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase());
+ viewPagerAdapter.setTabData(fragmentList, titleList);
+ viewPagerAdapter.notifyDataSetChanged();
+
+ }
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ compositeDisposable.clear();
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ViewPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/profile/ViewPagerAdapter.java
new file mode 100644
index 000000000..16a6197fe
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/ViewPagerAdapter.java
@@ -0,0 +1,57 @@
+package fr.free.nrw.commons.profile;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This View Pager Adapter will set the fragments for profile activity
+ */
+public class ViewPagerAdapter extends FragmentPagerAdapter {
+ private List fragmentList = new ArrayList<>();
+ private List fragmentTitleList = new ArrayList<>();
+
+ public ViewPagerAdapter(FragmentManager manager) {
+ super(manager);
+ }
+
+ /**
+ * This method returns the fragment of the viewpager at a particular position
+ * @param position
+ */
+ @Override
+ public Fragment getItem(int position) {
+ return fragmentList.get(position);
+ }
+
+ /**
+ * This method returns the total number of fragments in the viewpager.
+ * @return size
+ */
+ @Override
+ public int getCount() {
+ return fragmentList.size();
+ }
+
+ /**
+ * This method sets the fragment and title list in the viewpager
+ * @param fragmentList List of all fragments to be displayed in the viewpager
+ * @param fragmentTitleList List of all titles of the fragments
+ */
+ public void setTabData(List fragmentList, List fragmentTitleList) {
+ this.fragmentList = fragmentList;
+ this.fragmentTitleList = fragmentTitleList;
+ }
+
+ /**
+ * This method returns the title of the page at a particular position
+ * @param position
+ */
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return fragmentTitleList.get(position);
+ }
+}
+
diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/Achievements.kt
similarity index 85%
rename from app/src/main/java/fr/free/nrw/commons/achievements/Achievements.kt
rename to app/src/main/java/fr/free/nrw/commons/profile/achievements/Achievements.kt
index 3de252af8..081fe7e5f 100644
--- a/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.kt
+++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/Achievements.kt
@@ -1,4 +1,4 @@
-package fr.free.nrw.commons.achievements
+package fr.free.nrw.commons.profile.achievements
/**
* Represents Achievements class and stores all the parameters
@@ -87,12 +87,14 @@ class Achievements {
*/
@JvmStatic
fun from(response: FeedbackResponse): Achievements {
- return Achievements(response.uniqueUsedImages,
- response.articlesUsingImages,
- response.thanksReceived,
- response.featuredImages.qualityImages
- + response.featuredImages.featuredPicturesOnWikimediaCommons, 0,
- response.deletedUploads)
+ return Achievements(
+ response.uniqueUsedImages,
+ response.articlesUsingImages,
+ response.thanksReceived,
+ response.featuredImages.qualityImages
+ + response.featuredImages.featuredPicturesOnWikimediaCommons, 0,
+ response.deletedUploads
+ )
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java
similarity index 86%
rename from app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java
rename to app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java
index d12ec942e..cb0e924b4 100644
--- a/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java
@@ -1,61 +1,52 @@
-package fr.free.nrw.commons.achievements;
+package fr.free.nrw.commons.profile.achievements;
import android.accounts.Account;
-import android.annotation.SuppressLint;
-import android.content.Context;
+import android.app.AlertDialog;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.util.DisplayMetrics;
-import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.Menu;
+import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
-
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.widget.Toolbar;
+import androidx.appcompat.view.ContextThemeWrapper;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.FileProvider;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
-
-import com.dinuscxj.progressbar.CircleProgressBar;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.Objects;
-
-import javax.inject.Inject;
-
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
+import com.dinuscxj.progressbar.CircleProgressBar;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
-import fr.free.nrw.commons.theme.NavigationBaseActivity;
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.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+import javax.inject.Inject;
+import org.apache.commons.lang3.StringUtils;
import timber.log.Timber;
-
-
/**
- * activity for sharing feedback on uploaded activity
+ * fragment for sharing feedback on uploaded activity
*/
-public class AchievementsActivity extends NavigationBaseActivity {
+public class AchievementsFragment extends CommonsDaggerSupportFragment {
private static final double BADGE_IMAGE_WIDTH_RATIO = 0.4;
private static final double BADGE_IMAGE_HEIGHT_RATIO = 0.3;
@@ -64,55 +55,72 @@ public class AchievementsActivity extends NavigationBaseActivity {
@BindView(R.id.achievement_badge_image)
ImageView imageView;
+
@BindView(R.id.achievement_badge_text)
TextView badgeText;
+
@BindView(R.id.achievement_level)
TextView levelNumber;
- @BindView(R.id.toolbar)
- Toolbar toolbar;
+
@BindView(R.id.thanks_received)
TextView thanksReceived;
+
@BindView(R.id.images_uploaded_progressbar)
CircleProgressBar imagesUploadedProgressbar;
+
@BindView(R.id.images_used_by_wiki_progress_bar)
CircleProgressBar imagesUsedByWikiProgressBar;
+
@BindView(R.id.image_reverts_progressbar)
CircleProgressBar imageRevertsProgressbar;
+
@BindView(R.id.image_featured)
TextView imagesFeatured;
+
@BindView(R.id.images_revert_limit_text)
TextView imagesRevertLimitText;
+
@BindView(R.id.progressBar)
ProgressBar progressBar;
+
@BindView(R.id.layout_image_uploaded)
RelativeLayout layoutImageUploaded;
+
@BindView(R.id.layout_image_reverts)
RelativeLayout layoutImageReverts;
+
@BindView(R.id.layout_image_used_by_wiki)
RelativeLayout layoutImageUsedByWiki;
+
@BindView(R.id.layout_statistics)
LinearLayout layoutStatistics;
+
@BindView(R.id.images_used_by_wiki_text)
TextView imageByWikiText;
+
@BindView(R.id.images_reverted_text)
TextView imageRevertedText;
+
@BindView(R.id.images_upload_text_param)
TextView imageUploadedText;
+
@BindView(R.id.wikidata_edits)
TextView wikidataEditsText;
-
@Inject
SessionManager sessionManager;
+
@Inject
OkHttpJsonApiClient okHttpJsonApiClient;
- MenuItem item;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
// To keep track of the number of wiki edits made by a user
private int numberOfEdits = 0;
+ // menu item for action bar
+ private MenuItem item;
+
/**
* This method helps in the creation Achievement screen and
* dynamically set the size of imageView
@@ -120,15 +128,13 @@ public class AchievementsActivity extends NavigationBaseActivity {
* @param savedInstanceState Data bundle
*/
@Override
- @SuppressLint("StringFormatInvalid")
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_achievements);
- ButterKnife.bind(this);
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_achievements, container, false);
+ ButterKnife.bind(this, rootView);
// DisplayMetrics used to fetch the size of the screen
DisplayMetrics displayMetrics = new DisplayMetrics();
- getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+ getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int height = displayMetrics.heightPixels;
int width = displayMetrics.widthPixels;
@@ -139,37 +145,23 @@ public class AchievementsActivity extends NavigationBaseActivity {
params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO);
imageView.requestLayout();
- setSupportActionBar(toolbar);
progressBar.setVisibility(View.VISIBLE);
+ setHasOptionsMenu(true);
+
hideLayouts();
setWikidataEditCount();
setAchievements();
- initDrawer();
+ return rootView;
}
@Override
- public void onDestroy() {
- super.onDestroy();
- compositeDisposable.clear();
- }
-
- /**
- * To invoke the AlertDialog on clicking info button
- */
- @OnClick(R.id.achievement_info)
- public void showInfoDialog(){
- launchAlert(getResources().getString(R.string.Achievements)
- ,getResources().getString(R.string.achievements_info_message));
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
// Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.menu_about, menu);
- item=menu.getItem(0);
+ super.onCreateOptionsMenu(menu, menuInflater);
+ menuInflater.inflate(R.menu.menu_about, menu);
+ item = menu.getItem(0);
item.setVisible(false);
- return true;
}
/**
@@ -180,7 +172,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
int id = item.getItemId();
// take screenshot in form of bitmap and show it in Alert Dialog
if (id == R.id.share_app_icon) {
- View rootView = getWindow().getDecorView().findViewById(android.R.id.content);
+ View rootView = getActivity().getWindow().getDecorView().findViewById(android.R.id.content);
Bitmap screenShot = Utils.getScreenShot(rootView);
showAlert(screenShot);
}
@@ -188,20 +180,39 @@ public class AchievementsActivity extends NavigationBaseActivity {
return super.onOptionsItemSelected(item);
}
+ /**
+ * It displays the alertDialog with Image of screenshot
+ * @param screenshot
+ */
+ public void showAlert(Bitmap screenshot){
+ AlertDialog.Builder alertadd = new AlertDialog.Builder(getActivity());
+ LayoutInflater factory = LayoutInflater.from(getActivity());
+ final View view = factory.inflate(R.layout.image_alert_layout, null);
+ ImageView screenShotImage = view.findViewById(R.id.alert_image);
+ screenShotImage.setImageBitmap(screenshot);
+ TextView shareMessage = view.findViewById(R.id.alert_text);
+ shareMessage.setText(R.string.achievements_share_message);
+ alertadd.setView(view);
+ alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot));
+ alertadd.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel());
+ alertadd.show();
+ }
+
/**
* To take bitmap and store it temporary storage and share it
* @param bitmap
*/
void shareScreen(Bitmap bitmap) {
try {
- File file = new File(this.getExternalCacheDir(), "screen.png");
+ File file = new File(getActivity().getExternalCacheDir(), "screen.png");
FileOutputStream fOut = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
fOut.flush();
fOut.close();
file.setReadable(true, false);
- Uri fileUri = FileProvider.getUriForFile(getApplicationContext(), getPackageName()+".provider", file);
- grantUriPermission(getPackageName(), fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ Uri fileUri = FileProvider
+ .getUriForFile(getActivity().getApplicationContext(), getActivity().getPackageName()+".provider", file);
+ getActivity().grantUriPermission(getActivity().getPackageName(), fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
@@ -212,6 +223,15 @@ public class AchievementsActivity extends NavigationBaseActivity {
}
}
+ /**
+ * To invoke the AlertDialog on clicking info button
+ */
+ @OnClick(R.id.achievement_info)
+ public void showInfoDialog(){
+ launchAlert(getResources().getString(R.string.Achievements)
+ ,getResources().getString(R.string.achievements_info_message));
+ }
+
/**
* To call the API to get results in form Single
* which then calls parseJson when results are fetched
@@ -234,7 +254,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
layoutImageReverts.setVisibility(View.INVISIBLE);
imageView.setVisibility(View.INVISIBLE);
// If the number of edits made by the user are more than 150,000
- // in some cases such high number of wiki edit counts cause the
+ // in some cases such high number of wiki edit counts cause the
// achievements calculator to fail in some cases, for more details
// refer Issue: #3295
if (numberOfEdits <= 150000) {
@@ -264,7 +284,6 @@ public class AchievementsActivity extends NavigationBaseActivity {
* To call the API to fetch the count of wiki data edits
* in the form of JavaRx Single object
*/
- @SuppressLint("CheckResult")
private void setWikidataEditCount() {
String userName = sessionManager.getUserName();
if (StringUtils.isBlank(userName)) {
@@ -285,18 +304,18 @@ public class AchievementsActivity extends NavigationBaseActivity {
/**
* Shows a snack bar which has an action button which on click dismisses the snackbar and invokes the
* listener passed
- * @param tooManyAchievements if this value is true it means that the number of achievements of the
- * user are so high that it wrecks havoc with the Achievements calculator due to which request may time
+ * @param tooManyAchievements if this value is true it means that the number of achievements of the
+ * user are so high that it wrecks havoc with the Achievements calculator due to which request may time
* out. Well this is the Ultimate Achievement
*/
private void showSnackBarWithRetry(boolean tooManyAchievements) {
if (tooManyAchievements) {
progressBar.setVisibility(View.GONE);
- ViewUtil.showDismissibleSnackBar(findViewById(android.R.id.content),
+ ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content),
R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements());
} else {
progressBar.setVisibility(View.GONE);
- ViewUtil.showDismissibleSnackBar(findViewById(android.R.id.content),
+ ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content),
R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements());
}
}
@@ -305,7 +324,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
* Shows a generic error toast when error occurs while loading achievements or uploads
*/
private void onError() {
- ViewUtil.showLongToast(this, getResources().getString(R.string.error_occurred));
+ ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred));
progressBar.setVisibility(View.GONE);
}
@@ -355,7 +374,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
}
private void setZeroAchievements() {
- AlertDialog.Builder builder=new AlertDialog.Builder(this)
+ AlertDialog.Builder builder=new AlertDialog.Builder(getActivity())
.setMessage(getString(R.string.no_achievements_yet))
.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
});
@@ -399,20 +418,10 @@ public class AchievementsActivity extends NavigationBaseActivity {
levelUpInfoString += " " + levelInfo.getLevelNumber();
levelNumber.setText(levelUpInfoString);
imageView.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge,
- new ContextThemeWrapper(this, levelInfo.getLevelStyle()).getTheme()));
+ new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme()));
badgeText.setText(Integer.toString(levelInfo.getLevelNumber()));
}
- /**
- * Creates a way to change current activity to AchievementActivity
- * @param context
- */
- public static void startYourself(Context context) {
- Intent intent = new Intent(context, AchievementsActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
- context.startActivity(intent);
- }
-
/**
* to hide progressbar
*/
@@ -447,24 +456,6 @@ public class AchievementsActivity extends NavigationBaseActivity {
levelNumber.setVisibility(View.INVISIBLE);
}
- /**
- * It display the alertDialog with Image of screenshot
- * @param screenshot
- */
- public void showAlert(Bitmap screenshot){
- AlertDialog.Builder alertadd = new AlertDialog.Builder(AchievementsActivity.this);
- LayoutInflater factory = LayoutInflater.from(AchievementsActivity.this);
- final View view = factory.inflate(R.layout.image_alert_layout, null);
- ImageView screenShotImage = view.findViewById(R.id.alert_image);
- screenShotImage.setImageBitmap(screenshot);
- TextView shareMessage = view.findViewById(R.id.alert_text);
- shareMessage.setText(R.string.achievements_share_message);
- alertadd.setView(view);
- alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot));
- alertadd.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel());
- alertadd.show();
- }
-
@OnClick(R.id.images_upload_info)
public void showUploadInfo(){
launchAlert(getResources().getString(R.string.images_uploaded)
@@ -507,7 +498,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
* @param message
*/
private void launchAlert(String title, String message){
- new AlertDialog.Builder(AchievementsActivity.this)
+ new AlertDialog.Builder(getActivity())
.setTitle(title)
.setMessage(message)
.setCancelable(true)
@@ -524,11 +515,11 @@ public class AchievementsActivity extends NavigationBaseActivity {
Account currentAccount = sessionManager.getCurrentAccount();
if (currentAccount == null) {
Timber.d("Current account is null");
- ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in));
- sessionManager.forceLogin(this);
+ ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.user_not_logged_in));
+ sessionManager.forceLogin(getActivity());
return false;
}
return true;
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/FeaturedImages.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeaturedImages.kt
similarity index 58%
rename from app/src/main/java/fr/free/nrw/commons/achievements/FeaturedImages.kt
rename to app/src/main/java/fr/free/nrw/commons/profile/achievements/FeaturedImages.kt
index 4f5351e3c..0a54a778f 100644
--- a/app/src/main/java/fr/free/nrw/commons/achievements/FeaturedImages.kt
+++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeaturedImages.kt
@@ -1,11 +1,11 @@
-package fr.free.nrw.commons.achievements
+package fr.free.nrw.commons.profile.achievements
import com.google.gson.annotations.SerializedName
/**
-* Represents Featured Images on WikiMedia Commons platform
-* Used by Achievements and FeedbackResponse (objects) of the user
-*/
+ * Represents Featured Images on WikiMedia Commons platform
+ * Used by Achievements and FeedbackResponse (objects) of the user
+ */
class FeaturedImages(
@field:SerializedName("Quality_images") val qualityImages: Int,
@field:SerializedName("Featured_pictures_on_Wikimedia_Commons") val featuredPicturesOnWikimediaCommons: Int
diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/FeedbackResponse.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeedbackResponse.kt
similarity index 66%
rename from app/src/main/java/fr/free/nrw/commons/achievements/FeedbackResponse.kt
rename to app/src/main/java/fr/free/nrw/commons/profile/achievements/FeedbackResponse.kt
index 8d5d8b7bd..f86ca3e9b 100644
--- a/app/src/main/java/fr/free/nrw/commons/achievements/FeedbackResponse.kt
+++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeedbackResponse.kt
@@ -1,8 +1,8 @@
-package fr.free.nrw.commons.achievements
+package fr.free.nrw.commons.profile.achievements
/**
-* Represent the Feedback Response of the user
-*/
+ * Represent the Feedback Response of the user
+ */
data class FeedbackResponse(val uniqueUsedImages: Int,
val articlesUsingImages: Int,
val deletedUploads: Int,
diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/LevelController.kt
similarity index 97%
rename from app/src/main/java/fr/free/nrw/commons/achievements/LevelController.kt
rename to app/src/main/java/fr/free/nrw/commons/profile/achievements/LevelController.kt
index 772f716bd..414bf271d 100644
--- a/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.kt
+++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/LevelController.kt
@@ -1,4 +1,4 @@
-package fr.free.nrw.commons.achievements
+package fr.free.nrw.commons.profile.achievements
import fr.free.nrw.commons.R
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
new file mode 100644
index 000000000..409450d60
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java
@@ -0,0 +1,125 @@
+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/DataSourceFactory.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java
new file mode 100644
index 000000000..b2965785a
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java
@@ -0,0 +1,110 @@
+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/LeaderboardConstants.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java
new file mode 100644
index 000000000..9b0fa0f6e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java
@@ -0,0 +1,45 @@
+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 = 10;
+
+ /**
+ * 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/LeaderboardFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java
new file mode 100644
index 000000000..305907aff
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java
@@ -0,0 +1,277 @@
+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.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.ProgressBar;
+import android.widget.Spinner;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.MergeAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.auth.SessionManager;
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
+import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
+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 {
+
+ @BindView(R.id.leaderboard_list)
+ RecyclerView leaderboardListRecyclerView;
+
+ @BindView(R.id.progressBar)
+ ProgressBar progressBar;
+
+ @BindView(R.id.category_spinner)
+ Spinner categorySpinner;
+
+ @BindView(R.id.duration_spinner)
+ Spinner durationSpinner;
+
+ @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;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_leaderboard, container, false);
+ ButterKnife.bind(this, rootView);
+
+ progressBar.setVisibility(View.VISIBLE);
+ hideLayouts();
+ 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);
+
+ durationSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> adapterView, View view, int i, long l) {
+
+ duration = durationValues[durationSpinner.getSelectedItemPosition()];
+ refreshLeaderboard();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> adapterView) {
+ }
+ });
+
+ categorySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> adapterView, View view, int i, long l) {
+ category = categoryValues[categorySpinner.getSelectedItemPosition()];
+ refreshLeaderboard();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> adapterView) {
+ }
+ });
+
+ return rootView;
+ }
+
+ /**
+ * Refreshes the leaderboard list
+ */
+ private void refreshLeaderboard() {
+ if (viewModel != null) {
+ viewModel.refresh(duration, category, limit, offset);
+ setLeaderboard(duration, category, limit, offset);
+ }
+ }
+
+ /**
+ * 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);
+ 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);
+ 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(sessionManager.getCurrentAccount()).name,
+ duration, category, null, null)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ response -> {
+ if (response != null && response.getStatus() == 200) {
+ 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());
+ leaderboardListRecyclerView.setLayoutManager(linearLayoutManager);
+ leaderboardListRecyclerView.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();
+ }
+ });
+ }
+
+ /**
+ * to hide progressbar
+ */
+ private void hideProgressBar() {
+ if (progressBar != null) {
+ progressBar.setVisibility(View.GONE);
+ categorySpinner.setVisibility(View.VISIBLE);
+ durationSpinner.setVisibility(View.VISIBLE);
+ leaderboardListRecyclerView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * to show progressbar
+ */
+ private void showProgressBar() {
+ if (progressBar != null) {
+ progressBar.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * used to hide the layouts while fetching results from api
+ */
+ private void hideLayouts(){
+ categorySpinner.setVisibility(View.INVISIBLE);
+ durationSpinner.setVisibility(View.INVISIBLE);
+ leaderboardListRecyclerView.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));
+ progressBar.setVisibility(View.GONE);
+ }
+
+}
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
new file mode 100644
index 000000000..5558f3d9e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java
@@ -0,0 +1,137 @@
+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/LeaderboardListAdapter.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java
new file mode 100644
index 000000000..753abbac1
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java
@@ -0,0 +1,89 @@
+package fr.free.nrw.commons.profile.leaderboard;
+
+import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.USER_LINK_PREFIX;
+
+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.Utils;
+
+/**
+ * This class extends RecyclerView.Adapter and creates the List section of the leaderboard
+ */
+public class LeaderboardListAdapter extends PagedListAdapter {
+
+ protected 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());
+
+ /*
+ Open the user profile in a webview when a username is clicked on leaderboard
+ */
+ holder.itemView.setOnClickListener(view -> Utils.handleWebUrl(holder.getContext(), Uri.parse(
+ String.format("%s%s", USER_LINK_PREFIX, getItem(position).getUsername()))));
+ }
+}
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
new file mode 100644
index 000000000..909b4f646
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java
@@ -0,0 +1,107 @@
+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/LeaderboardResponse.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java
new file mode 100644
index 000000000..34294fca9
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java
@@ -0,0 +1,237 @@
+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/UpdateAvatarResponse.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java
new file mode 100644
index 000000000..15449a488
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java
@@ -0,0 +1,77 @@
+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/UserDetailAdapter.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java
new file mode 100644
index 000000000..f5e31b782
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java
@@ -0,0 +1,93 @@
+package fr.free.nrw.commons.profile.leaderboard;
+
+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.recyclerview.widget.RecyclerView;
+import com.facebook.drawee.view.SimpleDraweeView;
+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;
+
+ 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()));
+
+ }
+
+ @Override
+ public int getItemCount() {
+ return 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
new file mode 100644
index 000000000..fece77110
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java
@@ -0,0 +1,41 @@
+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/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java
index 0019206dd..0dea4bf55 100644
--- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java
@@ -26,6 +26,7 @@ import androidx.drawerlayout.widget.DrawerLayout;
import com.google.android.material.navigation.NavigationView;
+import fr.free.nrw.commons.profile.ProfileActivity;
import org.wikipedia.dataclient.Service;
import javax.inject.Inject;
@@ -37,7 +38,6 @@ import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity;
-import fr.free.nrw.commons.achievements.AchievementsActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.LogoutClient;
import fr.free.nrw.commons.bookmarks.BookmarksActivity;
@@ -140,7 +140,7 @@ public abstract class NavigationBaseActivity extends BaseActivity
LinearLayout userIcon = navHeaderView.findViewById(R.id.user_details);
userIcon.setOnClickListener(v -> {
drawerLayout.closeDrawer(navigationView);
- AchievementsActivity.startYourself(NavigationBaseActivity.this);
+ ProfileActivity.startYourself(NavigationBaseActivity.this);
});
}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
index f118164bf..17ec83412 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
@@ -7,11 +7,9 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.Uri;
-
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.exifinterface.media.ExifInterface;
-
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
@@ -21,13 +19,15 @@ import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
-
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.location.LatLng;
+import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.schedulers.Schedulers;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.location.LatLng;
import timber.log.Timber;
/**
@@ -69,7 +69,9 @@ public class ImageUtils {
public static final int FILE_NAME_EXISTS = -4;
static final int NO_CATEGORY_SELECTED = -5;
- private static ProgressDialog progressDialog;
+ private static ProgressDialog progressDialogWallpaper;
+
+ private static ProgressDialog progressDialogAvatar;
@IntDef(
flag = true,
@@ -223,28 +225,78 @@ public class ImageUtils {
}, CallerThreadExecutor.getInstance());
}
+ /**
+ * Calls the set avatar api to set the image url as user's avatar
+ * @param context
+ * @param url
+ * @param username
+ * @param okHttpJsonApiClient
+ * @param compositeDisposable
+ */
+ public static void setAvatarFromImageUrl(Context context, String url, String username,
+ OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable) {
+ showSettingAvatarProgressBar(context);
+
+ try {
+ compositeDisposable.add(okHttpJsonApiClient
+ .setAvatar(username, url)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ response -> {
+ if (response != null && response.getStatus().equals("200")) {
+ ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_successfully));
+ if (progressDialogAvatar != null && progressDialogAvatar.isShowing()) {
+ progressDialogAvatar.dismiss();
+ }
+ }
+ },
+ t -> {
+ Timber.e(t, "Setting Avatar Failed");
+ ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully));
+ if (progressDialogAvatar != null) {
+ progressDialogAvatar.cancel();
+ }
+ }
+ ));
+ }
+ catch (Exception e){
+ Timber.d(e+"success");
+ ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully));
+ if (progressDialogAvatar != null) {
+ progressDialogAvatar.cancel();
+ }
+ }
+
+ }
+
private static void setWallpaper(Context context, Bitmap bitmap) {
WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
try {
wallpaperManager.setBitmap(bitmap);
ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully));
- if (progressDialog != null && progressDialog.isShowing()) {
- progressDialog.dismiss();
+ if (progressDialogWallpaper != null && progressDialogWallpaper.isShowing()) {
+ progressDialogWallpaper.dismiss();
}
} catch (IOException e) {
Timber.e(e, "Error setting wallpaper");
ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_unsuccessfully));
- if (progressDialog != null) {
- progressDialog.cancel();
+ if (progressDialogWallpaper != null) {
+ progressDialogWallpaper.cancel();
}
}
}
private static void showSettingWallpaperProgressBar(Context context) {
- progressDialog = ProgressDialog.show(context, context.getString(R.string.setting_wallpaper_dialog_title),
+ progressDialogWallpaper = ProgressDialog.show(context, context.getString(R.string.setting_wallpaper_dialog_title),
context.getString(R.string.setting_wallpaper_dialog_message), true);
}
+ private static void showSettingAvatarProgressBar(Context context) {
+ progressDialogAvatar = ProgressDialog.show(context, context.getString(R.string.setting_avatar_dialog_title),
+ context.getString(R.string.setting_avatar_dialog_message), true);
+ }
+
/**
* Result variable is a result of an or operation of all possible problems. Ie. if result
* is 0001 means IMAGE_DARK
diff --git a/app/src/main/res/layout/activity_achievements.xml b/app/src/main/res/layout/activity_achievements.xml
deleted file mode 100644
index 01bc98bbd..000000000
--- a/app/src/main/res/layout/activity_achievements.xml
+++ /dev/null
@@ -1,495 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml
new file mode 100644
index 000000000..3ce7386c4
--- /dev/null
+++ b/app/src/main/res/layout/activity_profile.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_achievements.xml b/app/src/main/res/layout/fragment_achievements.xml
new file mode 100644
index 000000000..506d34418
--- /dev/null
+++ b/app/src/main/res/layout/fragment_achievements.xml
@@ -0,0 +1,482 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_leaderboard.xml b/app/src/main/res/layout/fragment_leaderboard.xml
new file mode 100644
index 000000000..e5836b450
--- /dev/null
+++ b/app/src/main/res/layout/fragment_leaderboard.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/leaderboard_list_element.xml b/app/src/main/res/layout/leaderboard_list_element.xml
new file mode 100644
index 000000000..f34416942
--- /dev/null
+++ b/app/src/main/res/layout/leaderboard_list_element.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/leaderboard_user_element.xml b/app/src/main/res/layout/leaderboard_user_element.xml
new file mode 100644
index 000000000..c9337f5a7
--- /dev/null
+++ b/app/src/main/res/layout/leaderboard_user_element.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/fragment_image_detail.xml b/app/src/main/res/menu/fragment_image_detail.xml
index 21f5f9be9..7339834a1 100644
--- a/app/src/main/res/menu/fragment_image_detail.xml
+++ b/app/src/main/res/menu/fragment_image_detail.xml
@@ -25,5 +25,9 @@
android:id="@+id/menu_set_as_wallpaper"
android:title="@string/menu_set_wallpaper"
app:showAsAction="never" />
+
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 17365a5a5..89ce57701 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -46,4 +46,29 @@
- 1
- 0
+
+
+ - @string/leaderboard_upload
+ - @string/leaderboard_used
+ - @string/leaderboard_nearby
+
+
+
+ - upload
+ - used
+ - nearby
+
+
+
+ - @string/leaderboard_weekly
+ - @string/leaderboard_yearly
+ - @string/leaderboard_all_time
+
+
+
+ - weekly
+ - yearly
+ - all_time
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bf908f718..047be7267 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -399,6 +399,7 @@
Nominate For Deletion
Delete
Achievements
+ Profile
Statistics
Thanks Received
Featured Images
@@ -669,4 +670,22 @@ Upload your first media by tapping on the add button.
pause
resume
Paused
+ Achievements
+ Leaderboard
+ Rank:
+ Count:
+ Rank
+ User
+ Count
+ Set as leaderboard avatar
+ Setting as avatar, please wait
+ Avatar set
+ Error setting new avatar, please try again
+ Set as avatar
+ Yearly
+ Weekly
+ All time
+ Upload
+ Nearby
+ Used
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/delete/ReasonBuilderTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/delete/ReasonBuilderTest.kt
index e4bacb4d8..a10cb1a22 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/delete/ReasonBuilderTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/delete/ReasonBuilderTest.kt
@@ -3,10 +3,13 @@ package fr.free.nrw.commons.delete
import android.content.Context
import android.content.res.Resources
import fr.free.nrw.commons.Media
-import fr.free.nrw.commons.achievements.FeedbackResponse
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
+import fr.free.nrw.commons.profile.achievements.FeedbackResponse
+import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse
+import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse
import fr.free.nrw.commons.utils.ViewUtilWrapper
+import io.reactivex.Observable
import io.reactivex.Single
import media
import org.junit.Before
@@ -54,6 +57,10 @@ class ReasonBuilderTest {
`when`(sessionManager?.doesAccountExist()).thenReturn(true)
`when`(okHttpJsonApiClient!!.getAchievements(anyString()))
.thenReturn(Single.just(mock(FeedbackResponse::class.java)))
+ `when`(okHttpJsonApiClient!!.getLeaderboard(anyString(), anyString(), anyString(), anyString(), anyString()))
+ .thenReturn(Observable.just(mock(LeaderboardResponse::class.java)))
+ `when`(okHttpJsonApiClient!!.setAvatar(anyString(), anyString()))
+ .thenReturn(Single.just(mock(UpdateAvatarResponse::class.java)))
val media = media(filename="test_file", dateUploaded = Date())
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
new file mode 100644
index 000000000..51d806a88
--- /dev/null
+++ b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java
@@ -0,0 +1,116 @@
+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/UpdateAvatarApiTest.java b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java
new file mode 100644
index 000000000..7c2b25d3b
--- /dev/null
+++ b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java
@@ -0,0 +1,117 @@
+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/nearby/NearbyParentFragmentPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt
index 7b6ebd5d8..fa7f3bae7 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt
@@ -442,4 +442,4 @@ class NearbyParentFragmentPresenterTest {
verify(nearbyParentFragmentView).isNetworkConnectionEstablished()
verifyZeroInteractions(nearbyParentFragmentView)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/test/resources/leaderboard_sample_response.json b/app/src/test/resources/leaderboard_sample_response.json
new file mode 100644
index 000000000..04a205a93
--- /dev/null
+++ b/app/src/test/resources/leaderboard_sample_response.json
@@ -0,0 +1,19 @@
+{
+ "status": 200,
+ "username": "user",
+ "category_count": 0,
+ "limit": null,
+ "avatar": "avatar",
+ "offset": null,
+ "duration": "all_time",
+ "leaderboard_list": [
+ {
+ "username": "user",
+ "category_count": 0,
+ "avatar": "avatar",
+ "rank": 1
+ }
+ ],
+ "category": "used",
+ "rank": 1
+}
\ No newline at end of file
diff --git a/app/src/test/resources/update_leaderboard_avatar_sample_response.json b/app/src/test/resources/update_leaderboard_avatar_sample_response.json
new file mode 100644
index 000000000..e002ae742
--- /dev/null
+++ b/app/src/test/resources/update_leaderboard_avatar_sample_response.json
@@ -0,0 +1,5 @@
+{
+ "status": "200",
+ "message": "Avatar Updated",
+ "user": "user"
+}
\ No newline at end of file