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