diff --git a/app/build.gradle b/app/build.gradle index de49c6b93..ea06c5c43 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,6 +21,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.5.1' implementation 'info.debatty:java-string-similarity:0.24' implementation 'com.borjabravo:readmoretextview:2.1.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.0' implementation('com.mapbox.mapboxsdk:mapbox-android-sdk:5.5.0@aar') { transitive = true @@ -59,6 +60,7 @@ dependencies { testImplementation 'org.robolectric:robolectric:3.7.1' testImplementation 'com.nhaarman:mockito-kotlin:1.5.0' testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' + implementation 'com.dinuscxj:circleprogressbar:1.1.1' implementation 'com.caverock:androidsvg:1.2.1' implementation 'com.github.bumptech.glide:glide:4.7.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5bd0dc523..3665042ac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,3 +1,4 @@ + @@ -108,8 +109,11 @@ android:parentActivityName=".contributions.ContributionsActivity" /> - + + - + \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 9447d173a..4e4b46b01 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -2,11 +2,13 @@ package fr.free.nrw.commons; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; import android.net.Uri; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.customtabs.CustomTabsIntent; import android.support.v4.content.ContextCompat; +import android.view.View; import android.widget.Toast; import org.apache.commons.codec.binary.Hex; @@ -150,7 +152,7 @@ public class Utils { StringBuilder stringBuilder = new StringBuilder(); try { - String[] command = new String[] {"logcat","-d","-v","threadtime"}; + String[] command = new String[]{"logcat","-d","-v","threadtime"}; Process process = Runtime.getRuntime().exec(command); @@ -199,4 +201,18 @@ public class Utils { customTabsIntent.launchUrl(context, url); } + /** + * To take screenshot of the screen and return it in Bitmap format + * + * @param view + * @return + */ + public static Bitmap getScreenShot(View view) { + View screenView = view.getRootView(); + screenView.setDrawingCacheEnabled(true); + Bitmap bitmap = Bitmap.createBitmap(screenView.getDrawingCache()); + screenView.setDrawingCacheEnabled(false); + return bitmap; + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.java b/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.java new file mode 100644 index 000000000..5fac8da9a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.java @@ -0,0 +1,205 @@ +package fr.free.nrw.commons.achievements; + +import android.util.Log; + +/** + * represnts Achievements class ans stores all the parameters + */ +public class Achievements { + private int uniqueUsedImages; + private int articlesUsingImages; + private int thanksReceived; + private int imagesEditedBySomeoneElse; + private int featuredImages; + private int imagesUploaded; + private int revertCount; + + public Achievements(){ + + } + + /** + * constructor for achievements class to set its data members + * @param uniqueUsedImages + * @param articlesUsingImages + * @param thanksReceived + * @param imagesEditedBySomeoneElse + * @param featuredImages + * @param imagesUploaded + * @param revertCount + */ + public Achievements(int uniqueUsedImages, + int articlesUsingImages, + int thanksReceived, + int imagesEditedBySomeoneElse, + int featuredImages, + int imagesUploaded, + int revertCount) { + this.uniqueUsedImages = uniqueUsedImages; + this.articlesUsingImages = articlesUsingImages; + this.thanksReceived = thanksReceived; + this.imagesEditedBySomeoneElse = imagesEditedBySomeoneElse; + this.featuredImages = featuredImages; + this.imagesUploaded = imagesUploaded; + this.revertCount = revertCount; + } + + /** + * Builder class for Achievements class + */ + public class AchievementsBuilder { + private int nestedUniqueUsedImages; + private int nestedArticlesUsingImages; + private int nestedThanksReceived; + private int nestedImagesEditedBySomeoneElse; + private int nestedFeaturedImages; + private int nestedImagesUploaded; + private int nestedRevertCount; + + public AchievementsBuilder setUniqueUsedImages(int uniqueUsedImages) { + this.nestedUniqueUsedImages = uniqueUsedImages; + return this; + } + + public AchievementsBuilder setArticlesUsingImages(int articlesUsingImages) { + this.nestedArticlesUsingImages = articlesUsingImages; + return this; + } + + public AchievementsBuilder setThanksReceived(int thanksReceived) { + this.nestedThanksReceived = thanksReceived; + return this; + } + + public AchievementsBuilder setImagesEditedBySomeoneElse(int imagesEditedBySomeoneElse) { + this.nestedImagesEditedBySomeoneElse = imagesEditedBySomeoneElse; + return this; + } + + public AchievementsBuilder setFeaturedImages(int featuredImages) { + this.nestedFeaturedImages = featuredImages; + return this; + } + + public AchievementsBuilder setImagesUploaded(int imagesUploaded) { + this.nestedImagesUploaded = imagesUploaded; + return this; + } + + public AchievementsBuilder setRevertCount( int revertCount){ + this.nestedRevertCount = revertCount; + return this; + } + + public Achievements createAchievements(){ + return new Achievements(nestedUniqueUsedImages, + nestedArticlesUsingImages, + nestedThanksReceived, + nestedImagesEditedBySomeoneElse, + nestedFeaturedImages, + nestedImagesUploaded, + nestedRevertCount); + } + + } + + /** + * getter function to get count of images uploaded + * @return + */ + public int getImagesUploaded() { + return imagesUploaded; + } + + /** + * getter function to get count of featured images + * @return + */ + public int getFeaturedImages() { + return featuredImages; + } + + /** + * getter function to get count of thanks received + * @return + */ + public int getThanksReceived() { + return thanksReceived; + } + + /** + * getter function to get count of unique images used by wiki + * @return + */ + public int getUniqueUsedImages() { + return uniqueUsedImages; + } + + /** + * setter function to count of images uploaded + * @param imagesUploaded + */ + public void setImagesUploaded(int imagesUploaded) { + this.imagesUploaded = imagesUploaded; + } + + /** + * setter function to set count of featured images + * @param featuredImages + */ + public void setFeaturedImages(int featuredImages) { + this.featuredImages = featuredImages; + } + + /** + * setter function to set the count of images edited by someone + * @param imagesEditedBySomeoneElse + */ + public void setImagesEditedBySomeoneElse(int imagesEditedBySomeoneElse) { + this.imagesEditedBySomeoneElse = imagesEditedBySomeoneElse; + } + + /** + * setter function to set count of thanks received + * @param thanksReceived + */ + public void setThanksReceived(int thanksReceived) { + this.thanksReceived = thanksReceived; + } + + /** + * setter function to count of articles using images uploaded + * @param articlesUsingImages + */ + public void setArticlesUsingImages(int articlesUsingImages) { + this.articlesUsingImages = articlesUsingImages; + } + + /** + * setter function to set count of uniques images used by wiki + * @param uniqueUsedImages + */ + public void setUniqueUsedImages(int uniqueUsedImages) { + this.uniqueUsedImages = uniqueUsedImages; + } + + /** + * to set count of images reverted + * @param revertCount + */ + public void setRevertCount(int revertCount) { + this.revertCount = revertCount; + } + + /** + * used to calculate the percentages of images that haven't been reverted + * @return + */ + public int getNotRevertPercentage(){ + try { + return ((imagesUploaded - revertCount) * 100)/imagesUploaded; + } catch (ArithmeticException divideByZero ){ + return 100; + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java b/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java new file mode 100644 index 000000000..905d0dfbd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java @@ -0,0 +1,458 @@ +package fr.free.nrw.commons.achievements; + +import android.accounts.Account; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.content.res.ResourcesCompat; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.Toolbar; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.dinuscxj.progressbar.CircleProgressBar; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Optional; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +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 timber.log.Timber; + +/** + * activity for sharing feedback on uploaded activity + */ +public class AchievementsActivity extends NavigationBaseActivity { + + private static final double BADGE_IMAGE_WIDTH_RATIO = 0.4; + private static final double BADGE_IMAGE_HEIGHT_RATIO = 0.3; + private Boolean isUploadFetched = false; + private Boolean isStatisticsFetched = false; + private Boolean isRevertFetched = false; + private Achievements achievements = new Achievements(); + private LevelController.LevelInfo levelInfo; + + @BindView(R.id.achievement_badge) + ImageView imageView; + @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_progressbar) + CircleProgressBar imagesUsedByWikiProgessbar; + @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; + @Inject + SessionManager sessionManager; + @Inject + MediaWikiApi mediaWikiApi; + + private CompositeDisposable compositeDisposable = new CompositeDisposable(); + + /** + * This method helps in the creation Achievement screen and + * dynamically set the size of imageView + * + * @param savedInstanceState Data bundle + */ + @Override + @SuppressLint("StringFormatInvalid") + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_achievements); + ButterKnife.bind(this); + /** + * DisplayMetrics used to fetch the size of the screen + */ + DisplayMetrics displayMetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + int height = displayMetrics.heightPixels; + int width = displayMetrics.widthPixels; + + /** + * Used for the setting the size of imageView at runtime + */ + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) + imageView.getLayoutParams(); + params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO); + params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO); + imageView.setImageResource(R.drawable.badge); + imageView.requestLayout(); + + setSupportActionBar(toolbar); + progressBar.setVisibility(View.VISIBLE); + hideLayouts(); + setAchievements(); + setUploadCount(); + setRevertCount(); + initDrawer(); + } + + /** + * 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) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_about, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == R.id.share_app_icon) { + View rootView = getWindow().getDecorView().findViewById(android.R.id.content); + Bitmap screenShot = Utils.getScreenShot(rootView); + showAlert(screenShot); + } + + return super.onOptionsItemSelected(item); + } + + /** + * 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"); + FileOutputStream fOut = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut); + fOut.flush(); + fOut.close(); + file.setReadable(true, false); + final Intent intent = new Intent(android.content.Intent.ACTION_SEND); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)); + intent.setType("image/png"); + startActivity(Intent.createChooser(intent, "Share image via")); + } catch (IOException e) { + //Do Nothing + } + } + + /** + * To call the API to get results in form Single + * which then calls parseJson when results are fetched + */ + private void setAchievements() { + if(checkAccount()) { + compositeDisposable.add(mediaWikiApi + .getAchievements(sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + jsonObject -> parseJson(jsonObject), + t -> Timber.e(t, "Fetching achievements statisticss failed") + )); + } + } + + /** + * To call the API to get reverts count in form of JSONObject + * + */ + + private void setRevertCount(){ + if(checkAccount()) { + compositeDisposable.add(mediaWikiApi + .getRevertCount(sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + object -> parseJsonRevertCount(object), + t -> Timber.e(t, "Fetching revert count failed") + )); + } + } + + /** + * used to set number of deleted images + * @param object + */ + private void parseJsonRevertCount(JSONObject object){ + try { + achievements.setRevertCount(object.getInt("deletedUploads")); + } catch (JSONException e) { + Timber.d( e, e.getMessage()); + } + isRevertFetched = true; + hideProgressBar(); + } + + /** + * used to the count of images uploaded by user + */ + private void setUploadCount() { + if(checkAccount()) { + compositeDisposable.add(mediaWikiApi + .getUploadCount(sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + uploadCount -> setAchievementsUploadCount(uploadCount), + t -> Timber.e(t, "Fetching upload count failed") + )); + } + } + + /** + * used to set achievements upload count and call hideProgressbar + * @param uploadCount + */ + private void setAchievementsUploadCount(int uploadCount){ + achievements.setImagesUploaded(uploadCount); + isUploadFetched = true; + hideProgressBar(); + } + + /** + * used to the uploaded images progressbar + * @param uploadCount + */ + private void setUploadProgress(int uploadCount){ + imagesUploadedProgressbar.setProgress + (100*uploadCount/levelInfo.getMaxUploadCount()); + imagesUploadedProgressbar.setProgressTextFormatPattern + (uploadCount +"/" + levelInfo.getMaxUploadCount() ); + } + + /** + * used to set the non revert image percentage + * @param notRevertPercentage + */ + private void setImageRevertPercentage(int notRevertPercentage){ + imageRevertsProgressbar.setProgress(notRevertPercentage); + String revertPercentage = Integer.toString(notRevertPercentage); + imageRevertsProgressbar.setProgressTextFormatPattern(revertPercentage + "%%"); + imagesRevertLimitText.setText(getResources().getString(R.string.achievements_revert_limit_message)+ levelInfo.getMinNonRevertPercentage() + "%"); + } + + /** + * used to parse the JSONObject containing results + * @param object + */ + private void parseJson(JSONObject object) { + try { + achievements.setUniqueUsedImages(object.getInt("uniqueUsedImages")); + achievements.setArticlesUsingImages(object.getInt("articlesUsingImages")); + achievements.setThanksReceived(object.getInt("thanksReceived")); + achievements.setImagesEditedBySomeoneElse(object.getInt("imagesEditedBySomeoneElse")); + JSONObject featuredImages = object.getJSONObject("featuredImages"); + achievements.setFeaturedImages + (featuredImages.getInt("Quality_images") + + featuredImages.getInt("Featured_pictures_on_Wikimedia_Commons")); + } catch (JSONException e) { + e.printStackTrace(); + } + isStatisticsFetched = true; + hideProgressBar(); + } + + /** + * Used the inflate the fetched statistics of the images uploaded by user + * and assign badge and level + * @param achievements + */ + private void inflateAchievements(Achievements achievements ){ + thanksReceived.setText(Integer.toString(achievements.getThanksReceived())); + imagesUsedByWikiProgessbar.setProgress + (100*achievements.getUniqueUsedImages()/levelInfo.getMaxUniqueImages() ); + imagesUsedByWikiProgessbar.setProgressTextFormatPattern + (achievements.getUniqueUsedImages() + "/" + levelInfo.getMaxUniqueImages()); + imagesFeatured.setText(Integer.toString(achievements.getFeaturedImages())); + String levelUpInfoString = getString(R.string.level); + levelUpInfoString += " " + Integer.toString(levelInfo.getLevelNumber()); + levelNumber.setText(levelUpInfoString); + final ContextThemeWrapper wrapper = new ContextThemeWrapper(this, levelInfo.getLevelStyle()); + Drawable drawable = ResourcesCompat.getDrawable(getResources(), R.drawable.badge, wrapper.getTheme()); + Bitmap bitmap = BitmapUtils.drawableToBitmap(drawable); + BitmapDrawable bitmapImage = BitmapUtils.writeOnDrawable(bitmap, Integer.toString(levelInfo.getLevelNumber()),this); + imageView.setImageDrawable(bitmapImage); + } + + /** + * 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_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + context.startActivity(intent); + } + + /** + * to hide progressbar + */ + private void hideProgressBar() { + if (progressBar != null && isUploadFetched && isStatisticsFetched && isRevertFetched) { + levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(), + achievements.getUniqueUsedImages(), + achievements.getNotRevertPercentage()); + inflateAchievements(achievements); + setUploadProgress(achievements.getImagesUploaded()); + setImageRevertPercentage(achievements.getNotRevertPercentage()); + progressBar.setVisibility(View.GONE); + layoutImageReverts.setVisibility(View.VISIBLE); + layoutImageUploaded.setVisibility(View.VISIBLE); + layoutImageUsedByWiki.setVisibility(View.VISIBLE); + layoutStatistics.setVisibility(View.VISIBLE); + imageView.setVisibility(View.VISIBLE); + levelNumber.setVisibility(View.VISIBLE); + } + } + + /** + * used to hide the layouts while fetching results from api + */ + private void hideLayouts(){ + layoutImageUsedByWiki.setVisibility(View.INVISIBLE); + layoutImageUploaded.setVisibility(View.INVISIBLE); + layoutImageReverts.setVisibility(View.INVISIBLE); + layoutStatistics.setVisibility(View.INVISIBLE); + imageView.setVisibility(View.INVISIBLE); + 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 = (ImageView) view.findViewById(R.id.alert_image); + screenShotImage.setImageBitmap(screenshot); + TextView shareMessage = (TextView) view.findViewById(R.id.alert_text); + shareMessage.setText(R.string.achievements_share_message); + alertadd.setView(view); + alertadd.setPositiveButton("Proceed", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + shareScreen(screenshot); + } + }); + alertadd.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + alertadd.show(); + } + + @OnClick(R.id.images_upload_info) + public void showUploadInfo(){ + launchAlert(getResources().getString(R.string.images_uploaded) + ,getResources().getString(R.string.images_uploaded_explanation)); + } + + @OnClick(R.id.images_reverted_info) + public void showRevertedInfo(){ + launchAlert(getResources().getString(R.string.image_reverts) + ,getResources().getString(R.string.images_reverted_explanation)); + } + + @OnClick(R.id.images_used_by_wiki_info) + public void showUsedByWikiInfo(){ + launchAlert(getResources().getString(R.string.images_used_by_wiki) + ,getResources().getString(R.string.images_used_explanation)); + } + + /** + * takes title and message as input to display alerts + * @param title + * @param message + */ + private void launchAlert(String title, String message){ + new AlertDialog.Builder(AchievementsActivity.this) + .setTitle(title) + .setMessage(message) + .setCancelable(true) + .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) + .create() + .show(); + } + + /** + * 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(this, getResources().getString(R.string.user_not_logged_in)); + sessionManager.forceLogin(this); + return false; + } + return true; + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/BitmapUtils.java b/app/src/main/java/fr/free/nrw/commons/achievements/BitmapUtils.java new file mode 100644 index 000000000..b7400117d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/achievements/BitmapUtils.java @@ -0,0 +1,54 @@ +package fr.free.nrw.commons.achievements; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +public class BitmapUtils { + + /** + * write level Number on the badge + * @param bm + * @param text + * @return + */ + public static BitmapDrawable writeOnDrawable(Bitmap bm, String text, Context context){ + Bitmap.Config config = bm.getConfig(); + if(config == null){ + config = Bitmap.Config.ARGB_8888; + } + Bitmap bitmap = Bitmap.createBitmap(bm.getWidth(),bm.getHeight(),config); + Canvas canvas = new Canvas(bitmap); + canvas.drawBitmap(bm, 0, 0, null); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.WHITE); + paint.setTextSize(Math.round(canvas.getHeight()/2)); + paint.setTextAlign(Paint.Align.CENTER); + Rect rectText = new Rect(); + paint.getTextBounds(text,0, text.length(),rectText); + canvas.drawText(text, Math.round(canvas.getWidth()/2),Math.round(canvas.getHeight()/1.35), paint); + return new BitmapDrawable(context.getResources(), bitmap); + } + + /** + * Convert Drawable to bitmap + * @param drawable + * @return + */ + public static Bitmap drawableToBitmap (Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable)drawable).getBitmap(); + } + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java b/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java new file mode 100644 index 000000000..e0f84bbee --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.achievements; + +import android.util.Log; + +import fr.free.nrw.commons.R; + +/** + * calculates the level of the user + */ +public class LevelController { + + public LevelInfo level; + public enum LevelInfo{ + LEVEL_1(1, R.style.LevelOne, 5, 20, 85), + LEVEL_2(2, R.style.LevelTwo, 10, 30, 86), + LEVEL_3(3, R.style.LevelThree, 15,40, 87), + LEVEL_4(4, R.style.LevelFour,20,50, 88), + LEVEL_5(5, R.style.LevelFive, 25, 60, 89), + LEVEL_6(6,R.style.LevelOne,30,70, 90), + LEVEL_7(7, R.style.LevelTwo, 40, 80, 90), + LEVEL_8(8, R.style.LevelThree, 45, 90, 90), + LEVEL_9(9, R.style.LevelFour, 50, 100, 90), + LEVEL_10(10, R.style.LevelFive, 55, 110, 90), + LEVEL_11(11,R.style.LevelOne, 60, 120, 90), + LEVEL_12(12,R.style.LevelTwo,65 , 130, 90), + LEVEL_13(13,R.style.LevelThree, 70, 140, 90), + LEVEL_14(14,R.style.LevelFour, 75 , 150, 90), + LEVEL_15(15,R.style.LevelFive, 80, 160, 90); + + private int levelNumber; + private int levelStyle; + private int maxUniqueImages; + private int maxUploadCount; + private int minNonRevertPercentage; + + LevelInfo(int levelNumber, + int levelStyle, + int maxUniqueImages, + int maxUploadCount, + int minNonRevertPercentage) { + this.levelNumber = levelNumber; + this.levelStyle = levelStyle; + this.maxUniqueImages = maxUniqueImages; + this.maxUploadCount = maxUploadCount; + this.minNonRevertPercentage = minNonRevertPercentage; + } + + public static LevelInfo from(int imagesUploaded, + int uniqueImagesUsed, + int nonRevertRate) { + LevelInfo level = LEVEL_15; + + for (LevelInfo levelInfo : LevelInfo.values()) { + if (imagesUploaded < levelInfo.maxUploadCount + || uniqueImagesUsed < levelInfo.maxUniqueImages + || nonRevertRate < levelInfo.minNonRevertPercentage ) { + level = levelInfo; + return level; + } + } + return level; + } + + public int getLevelStyle() { + return levelStyle; + } + + public int getLevelNumber() { + return levelNumber; + } + + public int getMaxUniqueImages() { + return maxUniqueImages; + } + + public int getMaxUploadCount() { + return maxUploadCount; + } + + public int getMinNonRevertPercentage(){ + return minNonRevertPercentage; + } + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index b6c0a2046..7d7443b95 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -135,11 +135,6 @@ public class LoginActivity extends AccountAuthenticatorActivity { } } - public static void startYourself(Context context) { - Intent intent = new Intent(context, LoginActivity.class); - context.startActivity(intent); - } - private void forgotPassword() { Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); } @@ -445,4 +440,9 @@ public class LoginActivity extends AccountAuthenticatorActivity { loginButton.setEnabled(enabled); } } + + public static void startYourself(Context context) { + Intent intent = new Intent(context, LoginActivity.class); + context.startActivity(intent); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index 48248225e..70ffec55f 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -4,12 +4,14 @@ import dagger.Module; import dagger.android.ContributesAndroidInjector; import fr.free.nrw.commons.AboutActivity; 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.SignupActivity; import fr.free.nrw.commons.category.CategoryDetailsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.explore.SearchActivity; + import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; @@ -58,4 +60,8 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract CategoryDetailsActivity bindCategoryDetailsActivity(); + + @ContributesAndroidInjector + abstract AchievementsActivity bindAchievementsActivity(); + } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 495321cd2..6088b774b 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -23,6 +23,7 @@ import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.CoreProtocolPNames; import org.apache.http.util.EntityUtils; +import org.json.JSONObject; import org.mediawiki.api.ApiResult; import org.mediawiki.api.MWApi; import org.w3c.dom.Element; @@ -53,6 +54,10 @@ import fr.free.nrw.commons.notification.NotificationUtils; import in.yuvi.http.fluent.Http; import io.reactivex.Observable; import io.reactivex.Single; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; import timber.log.Timber; import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue; @@ -902,13 +907,11 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { .param("meta", "userinfo") .param("uiprop", "blockinfo") .get(); - if(result != null) { + if (result != null) { String blockEnd = result.getString("/api/query/userinfo/@blockexpiry"); - if(blockEnd.equals("infinite")) - { + if (blockEnd.equals("infinite")) { userBlocked = true; - } - else if (!blockEnd.isEmpty()) { + } else if (!blockEnd.isEmpty()) { Date endDate = parseMWDate(blockEnd); Date current = new Date(); userBlocked = endDate.after(current); @@ -922,6 +925,68 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { return userBlocked; } + /** + * This takes userName as input, which is then used to fetch the feedback/achievements + * statistics using OkHttp and JavaRx. This function return JSONObject + * @param userName + * @return + */ + @NonNull + @Override + public Single getAchievements(String userName) { + final String fetchAchievementUrlTemplate = + wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py"; + return Single.fromCallable(() -> { + String url = String.format( + Locale.ENGLISH, + fetchAchievementUrlTemplate, + new PageTitle(userName).getText()); + HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); + urlBuilder.addQueryParameter("user", userName); + Log.i("url", urlBuilder.toString()); + Request request = new Request.Builder() + .url(urlBuilder.toString()) + .build(); + OkHttpClient client = new OkHttpClient(); + Response response = client.newCall(request).execute(); + String jsonData = response.body().string(); + JSONObject jsonObject = new JSONObject(jsonData); + return jsonObject; + }); + + } + + /** + * This takes userName as input, which is then used to fetch the no of images deleted + * using OkHttp and JavaRx. This function return JSONObject + * @param userName + * @return + */ + @NonNull + @Override + public Single getRevertCount(String userName){ + final String fetchRevertCountUrlTemplate = + wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py"; + return Single.fromCallable(() -> { + String url = String.format( + Locale.ENGLISH, + fetchRevertCountUrlTemplate, + new PageTitle(userName).getText()); + HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); + urlBuilder.addQueryParameter("user", userName); + urlBuilder.addQueryParameter("fetch","deletedUploads"); + Log.i("url", urlBuilder.toString()); + Request request = new Request.Builder() + .url(urlBuilder.toString()) + .build(); + OkHttpClient client = new OkHttpClient(); + Response response = client.newCall(request).execute(); + String jsonData = response.body().string(); + JSONObject jsonRevertObject = new JSONObject(jsonData); + return jsonRevertObject; + }); + } + private Date parseMWDate(String mwDate) { SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC isoFormat.setTimeZone(TimeZone.getTimeZone("UTC")); diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index c8f6f4961..78c990372 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -3,6 +3,8 @@ package fr.free.nrw.commons.mwapi; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import org.json.JSONObject; + import java.io.IOException; import java.io.InputStream; import java.util.List; @@ -97,6 +99,12 @@ public interface MediaWikiApi { boolean isUserBlockedFromCommons(); + @NonNull + Single getAchievements(String userName); + + @NonNull + Single getRevertCount(String userName); + interface ProgressListener { void onProgress(long transferred, long total); } 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 7d975d2bf..8107a961a 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 @@ -15,6 +15,7 @@ import android.support.v7.widget.Toolbar; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; @@ -24,6 +25,7 @@ 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.AccountUtil; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; @@ -68,12 +70,19 @@ public abstract class NavigationBaseActivity extends BaseActivity View navHeaderView = navigationView.getHeaderView(0); TextView username = navHeaderView.findViewById(R.id.username); - AccountManager accountManager = AccountManager.get(this); Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE); if (allAccounts.length != 0) { username.setText(allAccounts[0].name); } + ImageView userIcon = navHeaderView.findViewById(R.id.user_icon); + userIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + drawerLayout.closeDrawer(navigationView); + AchievementsActivity.startYourself(NavigationBaseActivity.this); + } + }); } public void initBackButton() { diff --git a/app/src/main/res/drawable-mdpi/badge.xml b/app/src/main/res/drawable-mdpi/badge.xml new file mode 100644 index 000000000..3792e33cf --- /dev/null +++ b/app/src/main/res/drawable-mdpi/badge.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/app/src/main/res/drawable/featured.xml b/app/src/main/res/drawable/featured.xml new file mode 100644 index 000000000..e971c3446 --- /dev/null +++ b/app/src/main/res/drawable/featured.xml @@ -0,0 +1,1069 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_info_outline_blue_24dp.xml b/app/src/main/res/drawable/ic_info_outline_blue_24dp.xml new file mode 100644 index 000000000..e3f68ad02 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_outline_blue_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_person_black_24dp.xml b/app/src/main/res/drawable/ic_person_black_24dp.xml new file mode 100644 index 000000000..d7366bda0 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_black_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_thanks.xml b/app/src/main/res/drawable/ic_thanks.xml new file mode 100644 index 000000000..480b45c1b --- /dev/null +++ b/app/src/main/res/drawable/ic_thanks.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/layout/activity_achievements.xml b/app/src/main/res/layout/activity_achievements.xml new file mode 100644 index 000000000..293234f09 --- /dev/null +++ b/app/src/main/res/layout/activity_achievements.xml @@ -0,0 +1,324 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml index 9bd3ae3a3..53a2684a0 100644 --- a/app/src/main/res/layout/drawer_header.xml +++ b/app/src/main/res/layout/drawer_header.xml @@ -1,5 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/image_alert_layout.xml b/app/src/main/res/layout/image_alert_layout.xml new file mode 100644 index 000000000..37b2c5b67 --- /dev/null +++ b/app/src/main/res/layout/image_alert_layout.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 8d125aa61..a4c9ef716 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -23,4 +23,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1db51fd8b..bad958661 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -50,6 +50,7 @@ #E0E0E0 #424242 + #D6DCE0 #757575 #FFFFFF diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 1697853e8..bac816cd4 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -4,6 +4,7 @@ 16dp 16dp + 8dp 48dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 51e2717af..8873d06df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,8 +76,8 @@ Starting %1$d uploads - %1$d upload - %1$d uploads + %1$d upload + %1$d uploads No categories matching %1$s found Add categories to make your images more discoverable on Wikimedia Commons.\nStart typing to add categories. @@ -191,7 +191,7 @@ Unable to display more than 500 Set Recent Upload Limit Two factor authentication is currently not supported. - Do you really want to logout? + Do you really want to logout? Commons Logo Commons Website Commons Facebook Page @@ -306,5 +306,20 @@ Wallpaper set successfully! Are you sure you want to clear your search history? Search history deleted + + Achievements + STATISTICS + Thanks Received + Featured Images + LEVEL + Images Uploaded + Images Not Reverted + Images Used + Share your achievements with your friends! + Your level increases as you meet these requirements. Items in the "statistics" section do not count towards your level. + minimum required: + The number of images you have uploaded to Commons, via any upload software + The percentage of images you have uploaded to Commons that were not deleted + The number of images you have uploaded to Commons that were used in Wikimedia articles Login session expired, please log in again. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a8eb1d849..26d9c357b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -57,4 +57,34 @@ @android:color/transparent + + + + + + + + + + \ No newline at end of file