Merge 2.8 release with master (#1887)

* Add Traceur for getting meaningful RxJava stack traces (#1832)

* Hotfix for overwrite issue in 2.8.0  (#1838)

* This solution is an hotfix for overrite issue came back on 2.8.0 version. What I did is checking the extension, and if it is null, adding .jpg suffix. Because commons files always have suffixes, and we should compare file names after adding suffixes. Othervise overrides are possible.

* Check if file title includes an extension already, by checking if is there any dot in it.

* Fix logic error

* Add uncovered tests

* Remove unecessary line breaks

* Make Javadocs more explicit

* Versioning and changelog for v2.8.2 (#1842)

* Versioning for v2.8.2

* Changelog for v2.8.2

* Add logs in wiki data edit and session refresh flow (#1874)

* Fix logout (#1875)

* [WIP] Refactor feedback and quiz to reduce possibility of NPE (#1881)

* Refactor feedback and quiz to reduce possibility of NPE

* Handle throwables in quiz checker

* Minor refactoring

* Set Traceur to only work in DEBUG mode (#1884)

* Bug fix for uploaded images count in achievements activity (#1885)

* Versioning and changelog for v2.8.3 (#1886)

* Update changelog.md

* Versioning for v2.8.3
This commit is contained in:
Vivek Maskara 2018-09-07 05:50:41 +05:30 committed by GitHub
parent 13c377aa17
commit a63b9f88bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 284 additions and 252 deletions

View file

@ -1,5 +1,13 @@
# Wikimedia Commons for Android # Wikimedia Commons for Android
## v2.8.3
- Fixed issues with session tokens not being cleared in 2FA, which should reduce p18 edit failures as well
- Fixed crash caused by bug in fetching revert count
- Fixed crash potentially caused by Traceur library
## v2.8.2
- Fixed bug with uploads sent via Share being given .jpeg extensions and overwriting files of the same name
## v2.8.1 ## v2.8.1
- Fixed bug with category edits not being sent to server - Fixed bug with category edits not being sent to server

View file

@ -59,6 +59,8 @@ dependencies {
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
implementation 'com.dinuscxj:circleprogressbar:1.1.1' implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.tspoon.traceur:traceur:1.0.1'
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION" androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
@ -83,8 +85,8 @@ android {
defaultConfig { defaultConfig {
applicationId 'fr.free.nrw.commons' applicationId 'fr.free.nrw.commons'
versionCode 88 versionCode 90
versionName '2.8.1' versionName '2.8.3'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion project.minSdkVersion minSdkVersion project.minSdkVersion

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import android.annotation.SuppressLint;
import android.app.Application; import android.app.Application;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
@ -14,6 +15,8 @@ import com.facebook.imagepipeline.core.ImagePipelineConfig;
import com.facebook.stetho.Stetho; import com.facebook.stetho.Stetho;
import com.squareup.leakcanary.LeakCanary; import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher; import com.squareup.leakcanary.RefWatcher;
import com.tspoon.traceur.Traceur;
import com.tspoon.traceur.TraceurConfig;
import org.acra.ACRA; import org.acra.ACRA;
import org.acra.ReportingInteractionMode; import org.acra.ReportingInteractionMode;
@ -84,6 +87,12 @@ public class CommonsApplication extends Application {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
if (BuildConfig.DEBUG) {
//FIXME: Traceur should be disabled for release builds until error fixed
//See https://github.com/commons-app/apps-android-commons/issues/1877
Traceur.enableLogging();
}
ApplicationlessInjection ApplicationlessInjection
.getInstance(this) .getInstance(this)
.getCommonsApplicationComponent() .getCommonsApplicationComponent()
@ -152,6 +161,7 @@ public class CommonsApplication extends Application {
* @param context Application context * @param context Application context
* @param logoutListener Implementation of interface LogoutListener * @param logoutListener Implementation of interface LogoutListener
*/ */
@SuppressLint("CheckResult")
public void clearApplicationData(Context context, LogoutListener logoutListener) { public void clearApplicationData(Context context, LogoutListener logoutListener) {
File cacheDirectory = context.getCacheDir(); File cacheDirectory = context.getCacheDir();
File applicationDirectory = new File(cacheDirectory.getParent()); File applicationDirectory = new File(cacheDirectory.getParent());
@ -164,7 +174,7 @@ public class CommonsApplication extends Application {
} }
} }
sessionManager.clearAllAccounts() sessionManager.logout()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> { .subscribe(() -> {
@ -175,7 +185,6 @@ public class CommonsApplication extends Application {
applicationPrefs.edit().putBoolean("firstrun", false).apply(); applicationPrefs.edit().putBoolean("firstrun", false).apply();
otherPrefs.edit().clear().apply(); otherPrefs.edit().clear().apply();
updateAllDatabases(); updateAllDatabases();
logoutListener.onLogoutComplete(); logoutListener.onLogoutComplete();
}); });
} }

View file

@ -111,7 +111,7 @@ public class Utils {
} }
/** /**
* Fixing incorrect extension * Adds extension to filename. Converts to .jpg if system provides .jpeg, adds .jpg if no extension detected
* @param title File name * @param title File name
* @param extension Correct extension * @param extension Correct extension
* @return File with correct extension * @return File with correct extension
@ -128,6 +128,15 @@ public class Utils {
.endsWith("." + extension.toLowerCase(Locale.ENGLISH))) { .endsWith("." + extension.toLowerCase(Locale.ENGLISH))) {
title += "." + extension; title += "." + extension;
} }
// If extension is still null, make it jpg. (Hotfix for https://github.com/commons-app/apps-android-commons/issues/228)
// If title has an extension in it, if won't be true
// FIXME: .png uploads fail when uploaded via Share
if (extension == null && title.lastIndexOf(".")<=0) {
extension = "jpg";
title += "." + extension;
}
return title; return title;
} }

View file

@ -1,7 +1,5 @@
package fr.free.nrw.commons.achievements; package fr.free.nrw.commons.achievements;
import android.util.Log;
/** /**
* represnts Achievements class ans stores all the parameters * represnts Achievements class ans stores all the parameters
*/ */
@ -45,62 +43,20 @@ public class Achievements {
} }
/** /**
* Builder class for Achievements class * Get Achievements object from FeedbackResponse
*
* @param response
* @return
*/ */
public class AchievementsBuilder { public static Achievements from(FeedbackResponse response) {
private int nestedUniqueUsedImages; return new Achievements(response.getUniqueUsedImages(),
private int nestedArticlesUsingImages; response.getArticlesUsingImages(),
private int nestedThanksReceived; response.getThanksReceived(),
private int nestedImagesEditedBySomeoneElse; response.getImagesEditedBySomeoneElse(),
private int nestedFeaturedImages; response.getFeaturedImages().getQualityImages()
private int nestedImagesUploaded; + response.getFeaturedImages().getFeaturedPicturesOnWikimediaCommons(),
private int nestedRevertCount; 0,
response.getDeletedUploads());
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);
}
} }
/** /**

View file

@ -2,16 +2,9 @@ package fr.free.nrw.commons.achievements;
import android.accounts.Account; import android.accounts.Account;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap; 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.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
@ -20,7 +13,6 @@ import android.support.v4.content.res.ResourcesCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log;
import android.view.ContextThemeWrapper; import android.view.ContextThemeWrapper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@ -34,13 +26,10 @@ import android.widget.TextView;
import com.dinuscxj.progressbar.CircleProgressBar; import com.dinuscxj.progressbar.CircleProgressBar;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; import java.util.Objects;
import javax.inject.Inject; import javax.inject.Inject;
@ -65,10 +54,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
private static final double BADGE_IMAGE_WIDTH_RATIO = 0.4; private static final double BADGE_IMAGE_WIDTH_RATIO = 0.4;
private static final double BADGE_IMAGE_HEIGHT_RATIO = 0.3; 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; private LevelController.LevelInfo levelInfo;
@BindView(R.id.achievement_badge) @BindView(R.id.achievement_badge)
@ -140,8 +126,6 @@ public class AchievementsActivity extends NavigationBaseActivity {
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
hideLayouts(); hideLayouts();
setAchievements(); setAchievements();
setUploadCount();
setRevertCount();
initDrawer(); initDrawer();
} }
@ -203,60 +187,48 @@ public class AchievementsActivity extends NavigationBaseActivity {
private void setAchievements() { private void setAchievements() {
if(checkAccount()) { if(checkAccount()) {
compositeDisposable.add(mediaWikiApi compositeDisposable.add(mediaWikiApi
.getAchievements(sessionManager.getCurrentAccount().name) .getAchievements(Objects.requireNonNull(sessionManager.getCurrentAccount()).name)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
jsonObject -> parseJson(jsonObject), response -> {
t -> Timber.e(t, "Fetching achievements statisticss failed") if (response != null) {
setUploadCount(Achievements.from(response));
} else {
onError();
}
},
t -> {
Timber.e(t, "Fetching achievements statistics failed");
onError();
}
)); ));
} }
} }
/** /**
* To call the API to get reverts count in form of JSONObject * Shows a generic error toast when error occurs while loading achievements or uploads
*
*/ */
private void onError() {
private void setRevertCount(){ ViewUtil.showLongToast(this, getResources().getString(R.string.error_occurred));
if(checkAccount()) { progressBar.setVisibility(View.GONE);
compositeDisposable.add(mediaWikiApi
.getRevertRespObjectSingle(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 * used to the count of images uploaded by user
*/ */
private void setUploadCount() { private void setUploadCount(Achievements achievements) {
if(checkAccount()) { if(checkAccount()) {
compositeDisposable.add(mediaWikiApi compositeDisposable.add(mediaWikiApi
.getUploadCount(sessionManager.getCurrentAccount().name) .getUploadCount(Objects.requireNonNull(sessionManager.getCurrentAccount()).name)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
uploadCount -> setAchievementsUploadCount(uploadCount), uploadCount -> setAchievementsUploadCount(achievements, uploadCount),
t -> Timber.e(t, "Fetching upload count failed") t -> {
Timber.e(t, "Fetching upload count failed");
onError();
}
)); ));
} }
} }
@ -265,10 +237,9 @@ public class AchievementsActivity extends NavigationBaseActivity {
* used to set achievements upload count and call hideProgressbar * used to set achievements upload count and call hideProgressbar
* @param uploadCount * @param uploadCount
*/ */
private void setAchievementsUploadCount(int uploadCount){ private void setAchievementsUploadCount(Achievements achievements, int uploadCount) {
achievements.setImagesUploaded(uploadCount); achievements.setImagesUploaded(uploadCount);
isUploadFetched = true; hideProgressBar(achievements);
hideProgressBar();
} }
/** /**
@ -293,27 +264,6 @@ public class AchievementsActivity extends NavigationBaseActivity {
imagesRevertLimitText.setText(getResources().getString(R.string.achievements_revert_limit_message)+ levelInfo.getMinNonRevertPercentage() + "%"); 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 * Used the inflate the fetched statistics of the images uploaded by user
* and assign badge and level * and assign badge and level
@ -350,8 +300,8 @@ public class AchievementsActivity extends NavigationBaseActivity {
/** /**
* to hide progressbar * to hide progressbar
*/ */
private void hideProgressBar() { private void hideProgressBar(Achievements achievements) {
if (progressBar != null && isUploadFetched && isStatisticsFetched && isRevertFetched) { if (progressBar != null) {
levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(), levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(),
achievements.getUniqueUsedImages(), achievements.getUniqueUsedImages(),
achievements.getNotRevertPercentage()); achievements.getNotRevertPercentage());
@ -393,17 +343,8 @@ public class AchievementsActivity extends NavigationBaseActivity {
TextView shareMessage = (TextView) view.findViewById(R.id.alert_text); TextView shareMessage = (TextView) view.findViewById(R.id.alert_text);
shareMessage.setText(R.string.achievements_share_message); shareMessage.setText(R.string.achievements_share_message);
alertadd.setView(view); alertadd.setView(view);
alertadd.setPositiveButton(R.string.about_translate_proceed, new DialogInterface.OnClickListener() { alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot));
public void onClick(DialogInterface dialog, int which) { alertadd.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel());
shareScreen(screenshot);
}
});
alertadd.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
alertadd.show(); alertadd.show();
} }

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.achievements;
import com.google.gson.annotations.SerializedName;
public class FeaturedImages {
@SerializedName("Quality_images")
private final int qualityImages;
@SerializedName("Featured_pictures_on_Wikimedia_Commons")
private final int featuredPicturesOnWikimediaCommons;
public FeaturedImages(int qualityImages, int featuredPicturesOnWikimediaCommons) {
this.qualityImages = qualityImages;
this.featuredPicturesOnWikimediaCommons = featuredPicturesOnWikimediaCommons;
}
public int getQualityImages() {
return qualityImages;
}
public int getFeaturedPicturesOnWikimediaCommons() {
return featuredPicturesOnWikimediaCommons;
}
}

View file

@ -0,0 +1,64 @@
package fr.free.nrw.commons.achievements;
public class FeedbackResponse {
private final String status;
private final int uniqueUsedImages;
private final int articlesUsingImages;
private final int deletedUploads;
private final FeaturedImages featuredImages;
private final int thanksReceived;
private final String user;
private final int imagesEditedBySomeoneElse;
public FeedbackResponse(String status,
int uniqueUsedImages,
int articlesUsingImages,
int deletedUploads,
FeaturedImages featuredImages,
int thanksReceived,
String user,
int imagesEditedBySomeoneElse) {
this.status = status;
this.uniqueUsedImages = uniqueUsedImages;
this.articlesUsingImages = articlesUsingImages;
this.deletedUploads = deletedUploads;
this.featuredImages = featuredImages;
this.thanksReceived = thanksReceived;
this.user = user;
this.imagesEditedBySomeoneElse = imagesEditedBySomeoneElse;
}
public String getStatus() {
return status;
}
public int getUniqueUsedImages() {
return uniqueUsedImages;
}
public int getArticlesUsingImages() {
return articlesUsingImages;
}
public int getDeletedUploads() {
return deletedUploads;
}
public FeaturedImages getFeaturedImages() {
return featuredImages;
}
public int getThanksReceived() {
return thanksReceived;
}
public String getUser() {
return user;
}
public int getImagesEditedBySomeoneElse() {
return imagesEditedBySomeoneElse;
}
}

View file

@ -153,11 +153,19 @@ public class SessionManager {
} }
} }
public Completable clearAllAccounts() { /**
* 1. Clears existing accounts from account manager
* 2. Calls MediaWikiApi's logout function to clear cookies
* @return
*/
public Completable logout() {
AccountManager accountManager = AccountManager.get(context); AccountManager accountManager = AccountManager.get(context);
Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE);
return Completable.fromObservable(Observable.fromArray(allAccounts) return Completable.fromObservable(Observable.fromArray(allAccounts)
.map(a -> accountManager.removeAccount(a, null, null).getResult())) .map(a -> accountManager.removeAccount(a, null, null).getResult()))
.doOnComplete(() -> currentAccount = null); .doOnComplete(() -> {
mediaWikiApi.logout();
currentAccount = null;
});
} }
} }

View file

@ -21,6 +21,7 @@ import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.upload.UploadController; import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.wikidata.WikidataEditListener; import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
import okhttp3.OkHttpClient;
import static android.content.Context.MODE_PRIVATE; import static android.content.Context.MODE_PRIVATE;

View file

@ -7,6 +7,8 @@ import android.support.annotation.NonNull;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import java.io.File;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -15,6 +17,7 @@ import dagger.Provides;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi; import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import okhttp3.Cache;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@ -25,8 +28,11 @@ public class NetworkingModule {
@Provides @Provides
@Singleton @Singleton
public OkHttpClient provideOkHttpClient() { public OkHttpClient provideOkHttpClient(Context context) {
return new OkHttpClient.Builder().build(); File dir = new File(context.getCacheDir(), "okHttpCache");
return new OkHttpClient.Builder()
.cache(new Cache(dir, OK_HTTP_CACHE_SIZE))
.build();
} }
@Provides @Provides
@ -34,8 +40,9 @@ public class NetworkingModule {
public MediaWikiApi provideMediaWikiApi(Context context, public MediaWikiApi provideMediaWikiApi(Context context,
@Named("default_preferences") SharedPreferences defaultPreferences, @Named("default_preferences") SharedPreferences defaultPreferences,
@Named("category_prefs") SharedPreferences categoryPrefs, @Named("category_prefs") SharedPreferences categoryPrefs,
Gson gson) { Gson gson,
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultPreferences, categoryPrefs, gson); OkHttpClient okHttpClient) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultPreferences, categoryPrefs, gson, okHttpClient);
} }
@Provides @Provides

View file

@ -24,7 +24,6 @@ import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams; import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreProtocolPNames; import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
import org.json.JSONObject;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.Node; import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
@ -45,6 +44,7 @@ import java.util.concurrent.Callable;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.category.CategoryImageUtils; import fr.free.nrw.commons.category.CategoryImageUtils;
import fr.free.nrw.commons.category.QueryContinue; import fr.free.nrw.commons.category.QueryContinue;
@ -76,14 +76,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private SharedPreferences defaultPreferences; private SharedPreferences defaultPreferences;
private SharedPreferences categoryPreferences; private SharedPreferences categoryPreferences;
private Gson gson; private Gson gson;
private final OkHttpClient okHttpClient;
public ApacheHttpClientMediaWikiApi(Context context, public ApacheHttpClientMediaWikiApi(Context context,
String apiURL, String apiURL,
String wikidatApiURL, String wikidatApiURL,
SharedPreferences defaultPreferences, SharedPreferences defaultPreferences,
SharedPreferences categoryPreferences, SharedPreferences categoryPreferences,
Gson gson) { Gson gson,
OkHttpClient okHttpClient) {
this.context = context; this.context = context;
this.okHttpClient = okHttpClient;
BasicHttpParams params = new BasicHttpParams(); BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry(); SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
@ -233,17 +236,27 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
@Override @Override
public String getCentralAuthToken() throws IOException { public String getCentralAuthToken() throws IOException {
String centralAuthToken = api.action("centralauthtoken") CustomApiResult result = api.action("centralauthtoken").get();
.get() String centralAuthToken = result.getString("/api/centralauthtoken/@centralauthtoken");
.getString("/api/centralauthtoken/@centralauthtoken");
Timber.d("MediaWiki Central auth token is %s", centralAuthToken); Timber.d("MediaWiki Central auth token is %s", centralAuthToken);
if(centralAuthToken == null || centralAuthToken.isEmpty()) { if ((centralAuthToken == null || centralAuthToken.isEmpty())
&& "notLoggedIn".equals(result.getString("api/error/@code"))) {
Timber.d("Central auth token isn't valid. Trying to fetch a fresh token");
api.removeAllCookies(); api.removeAllCookies();
String login = login(AccountUtil.getUserName(context), AccountUtil.getPassword(context)); String loginResultCode = login(AccountUtil.getUserName(context), AccountUtil.getPassword(context));
if(login.equals("PASS")) { if(loginResultCode.equals("PASS")) {
return getCentralAuthToken(); return getCentralAuthToken();
} else if(loginResultCode.equals("2FA")) {
Timber.e("Cannot refresh session for 2FA enabled user. Login required");
} else {
Timber.e("Error occurred in refreshing session. Error code is %s", loginResultCode);
} }
} else {
Timber.e("Error occurred while fetching auth token. Error code is %s and message is %s",
result.getString("api/error/@code"),
result.getString("api/error/@info"));
} }
return centralAuthToken; return centralAuthToken;
} }
@ -467,13 +480,12 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return false; return false;
} }
Node node = result.getNode("api").getDocument(); if ("success".equals(result.getString("api/tag/result/@status"))) {
Element element = (Element) node;
if (element != null && element.getAttribute("status").equals("success")) {
return true; return true;
} else { } else {
Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info")); Timber.e("Error occurred in creating claim. Error code is: %s and message is %s",
result.getString("api/error/@code"),
result.getString("api/error/@info"));
} }
return false; return false;
} }
@ -954,12 +966,11 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
/** /**
* This takes userName as input, which is then used to fetch the feedback/achievements * This takes userName as input, which is then used to fetch the feedback/achievements
* statistics using OkHttp and JavaRx. This function return JSONObject * statistics using OkHttp and JavaRx. This function return JSONObject
* @param userName * @param userName MediaWiki user name
* @return * @return
*/ */
@NonNull
@Override @Override
public Single<JSONObject> getAchievements(String userName) { public Single<FeedbackResponse> getAchievements(String userName) {
final String fetchAchievementUrlTemplate = final String fetchAchievementUrlTemplate =
wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py"; wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py";
return Single.fromCallable(() -> { return Single.fromCallable(() -> {
@ -973,44 +984,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
Request request = new Request.Builder() Request request = new Request.Builder()
.url(urlBuilder.toString()) .url(urlBuilder.toString())
.build(); .build();
OkHttpClient client = new OkHttpClient(); Response response = okHttpClient.newCall(request).execute();
Response response = client.newCall(request).execute(); if (response != null && response.body() != null && response.isSuccessful()) {
String jsonData = response.body().string(); String json = response.body().string();
JSONObject jsonObject = new JSONObject(jsonData); if (json == null) {
return jsonObject; return null;
});
} }
return gson.fromJson(json, FeedbackResponse.class);
/** }
* This takes userName as input, which is then used to fetch the no of images deleted return null;
* using OkHttp and JavaRx. This function return JSONObject
* @param userName
* @return
*/
@NonNull
@Override
public Single<JSONObject> getRevertRespObjectSingle(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) { private Date parseMWDate(String mwDate) {
@ -1023,4 +1007,15 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
} }
} }
/**
* Calls media wiki's logout API
*/
public void logout() {
try {
api.logout();
} catch (IOException e) {
Timber.e(e, "Error occurred while logging out");
}
}
} }

View file

@ -167,6 +167,8 @@ public class CustomMwApi {
// I should be doing more validation here, but meh // I should be doing more validation here, but meh
isLoggedIn = false; isLoggedIn = false;
this.action("logout").post(); this.action("logout").post();
removeAllCookies();
authCookie = null;
} }
private CustomApiResult makeRequest(String method, HashMap<String, Object> params) throws IOException { private CustomApiResult makeRequest(String method, HashMap<String, Object> params) throws IOException {

View file

@ -4,13 +4,12 @@ import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.notification.Notification; import fr.free.nrw.commons.notification.Notification;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
@ -98,13 +97,11 @@ public interface MediaWikiApi {
@NonNull @NonNull
Single<Integer> getUploadCount(String userName); Single<Integer> getUploadCount(String userName);
@NonNull
Single<JSONObject> getRevertRespObjectSingle(String userName);
boolean isUserBlockedFromCommons(); boolean isUserBlockedFromCommons();
@NonNull Single<FeedbackResponse> getAchievements(String userName);
Single<JSONObject> getAchievements(String userName);
void logout();
interface ProgressListener { interface ProgressListener {
void onProgress(long transferred, long total); void onProgress(long transferred, long total);

View file

@ -1,11 +1,9 @@
package fr.free.nrw.commons.quiz; package fr.free.nrw.commons.quiz;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.support.v7.app.AlertDialog.Builder; import android.support.v7.app.AlertDialog.Builder;
import android.util.Log;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.WelcomeActivity;
@ -34,16 +32,16 @@ public class QuizChecker {
private SharedPreferences revertPref; private SharedPreferences revertPref;
private SharedPreferences countPref; private SharedPreferences countPref;
private final int UPLOAD_COUNT_THRESHOLD = 5; private static final int UPLOAD_COUNT_THRESHOLD = 5;
private final String REVERT_PERCENTAGE_FOR_MESSAGE = "50%"; private static final String REVERT_PERCENTAGE_FOR_MESSAGE = "50%";
private final String REVERT_SHARED_PREFERENCE = "revertCount"; private final String REVERT_SHARED_PREFERENCE = "revertCount";
private final String UPLOAD_SHARED_PREFERENCE = "uploadCount"; private final String UPLOAD_SHARED_PREFERENCE = "uploadCount";
/** /**
* constructor to set the parameters for quiz * constructor to set the parameters for quiz
* @param context * @param context context
* @param userName * @param userName Commons user name
* @param mediaWikiApi * @param mediaWikiApi instance of MediaWikiApi
*/ */
public QuizChecker(Context context, String userName, MediaWikiApi mediaWikiApi) { public QuizChecker(Context context, String userName, MediaWikiApi mediaWikiApi) {
this.context = context; this.context = context;
@ -63,8 +61,7 @@ public class QuizChecker {
.getUploadCount(userName) .getUploadCount(userName)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(this::setTotalUploadCount,
uploadCount -> setTotalUploadCount(uploadCount),
t -> Timber.e(t, "Fetching upload count failed") t -> Timber.e(t, "Fetching upload count failed")
)); ));
} }
@ -72,7 +69,7 @@ public class QuizChecker {
/** /**
* set the sub Title of Contibutions Activity and * set the sub Title of Contibutions Activity and
* call function to check for quiz * call function to check for quiz
* @param uploadCount * @param uploadCount user's upload count
*/ */
private void setTotalUploadCount(int uploadCount) { private void setTotalUploadCount(int uploadCount) {
totalUploadCount = uploadCount - countPref.getInt(UPLOAD_SHARED_PREFERENCE,0); totalUploadCount = uploadCount - countPref.getInt(UPLOAD_SHARED_PREFERENCE,0);
@ -89,17 +86,21 @@ public class QuizChecker {
*/ */
private void setRevertCount() { private void setRevertCount() {
compositeDisposable.add(mediaWikiApi compositeDisposable.add(mediaWikiApi
.getRevertRespObjectSingle(userName) .getAchievements(userName)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
object -> setRevertParameter(object.getInt("deletedUploads")) response -> {
)); if (response != null) {
setRevertParameter(response.getDeletedUploads());
}
}, throwable -> Timber.e(throwable, "Fetching feedback failed"))
);
} }
/** /**
* to calculate the number of images reverted after previous quiz * to calculate the number of images reverted after previous quiz
* @param revertCountFetched * @param revertCountFetched count of deleted uploads
*/ */
private void setRevertParameter(int revertCountFetched) { private void setRevertParameter(int revertCountFetched) {
revertCount = revertCountFetched - revertPref.getInt(REVERT_SHARED_PREFERENCE,0); revertCount = revertCountFetched - revertPref.getInt(REVERT_SHARED_PREFERENCE,0);
@ -130,14 +131,12 @@ public class QuizChecker {
/** /**
* Alert which prompts to quiz * Alert which prompts to quiz
*/ */
public void callQuiz() { private void callQuiz() {
Builder alert = new Builder(context); Builder alert = new Builder(context);
alert.setTitle(context.getResources().getString(R.string.quiz)); alert.setTitle(context.getResources().getString(R.string.quiz));
alert.setMessage(context.getResources().getString(R.string.quiz_alert_message, alert.setMessage(context.getResources().getString(R.string.quiz_alert_message,
REVERT_PERCENTAGE_FOR_MESSAGE)); REVERT_PERCENTAGE_FOR_MESSAGE));
alert.setPositiveButton(R.string.about_translate_proceed, new DialogInterface.OnClickListener() { alert.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> {
@Override
public void onClick(DialogInterface dialog, int which) {
int newRevetSharedPrefs = revertCount + revertPref.getInt(REVERT_SHARED_PREFERENCE, 0); int newRevetSharedPrefs = revertCount + revertPref.getInt(REVERT_SHARED_PREFERENCE, 0);
revertPref.edit().putInt(REVERT_SHARED_PREFERENCE, newRevetSharedPrefs).apply(); revertPref.edit().putInt(REVERT_SHARED_PREFERENCE, newRevetSharedPrefs).apply();
int newUploadCount = totalUploadCount + countPref.getInt(UPLOAD_SHARED_PREFERENCE, 0); int newUploadCount = totalUploadCount + countPref.getInt(UPLOAD_SHARED_PREFERENCE, 0);
@ -146,14 +145,8 @@ public class QuizChecker {
i.putExtra("isQuiz", true); i.putExtra("isQuiz", true);
dialog.dismiss(); dialog.dismiss();
context.startActivity(i); context.startActivity(i);
}
});
alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.cancel();
}
}); });
alert.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
android.support.v7.app.AlertDialog dialog = alert.create(); android.support.v7.app.AlertDialog dialog = alert.create();
dialog.show(); dialog.show();
} }

View file

@ -351,6 +351,8 @@
<string name="images_uploaded_explanation">The number of images you have uploaded to Commons, via any upload software</string> <string name="images_uploaded_explanation">The number of images you have uploaded to Commons, via any upload software</string>
<string name="images_reverted_explanation">The percentage of images you have uploaded to Commons that were not deleted</string> <string name="images_reverted_explanation">The percentage of images you have uploaded to Commons that were not deleted</string>
<string name="images_used_explanation">The number of images you have uploaded to Commons that were used in Wikimedia articles</string> <string name="images_used_explanation">The number of images you have uploaded to Commons that were used in Wikimedia articles</string>
<string name="error_occurred">Error occurred!</string>
<string name="notifications_channel_name_all">Commons Notification</string> <string name="notifications_channel_name_all">Commons Notification</string>
<string name="storage_permission">Storage Permission</string> <string name="storage_permission">Storage Permission</string>
<string name="write_storage_permission_rationale_for_image_share">We need your permission to access the external storage of your device in order to upload images.</string> <string name="write_storage_permission_rationale_for_image_share">We need your permission to access the external storage of your device in order to upload images.</string>

View file

@ -65,4 +65,14 @@ class UtilsFixExtensionTest {
fun inWordJpegToJpgResultsInJpg() { fun inWordJpegToJpgResultsInJpg() {
assertEquals("X.jpeg.SAMPLE.jpg", fixExtension("X.jpeg.SAMPLE", "jpg")) assertEquals("X.jpeg.SAMPLE.jpg", fixExtension("X.jpeg.SAMPLE", "jpg"))
} }
@Test
fun noExtensionShouldResultInJpg() {
assertEquals("Sample.jpg", fixExtension("Sample", null))
}
@Test
fun extensionAlreadyInTitleShouldRemain() {
assertEquals("Sample.jpg", fixExtension("Sample.jpg", null))
}
} }

View file

@ -6,6 +6,7 @@ import android.preference.PreferenceManager
import com.google.gson.Gson import com.google.gson.Gson
import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.TestCommonsApplication
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest import okhttp3.mockwebserver.RecordedRequest
@ -30,14 +31,16 @@ class ApacheHttpClientMediaWikiApiTest {
private lateinit var wikidataServer: MockWebServer private lateinit var wikidataServer: MockWebServer
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var categoryPreferences: SharedPreferences private lateinit var categoryPreferences: SharedPreferences
private lateinit var okHttpClient: OkHttpClient
@Before @Before
fun setUp() { fun setUp() {
server = MockWebServer() server = MockWebServer()
wikidataServer = MockWebServer() wikidataServer = MockWebServer()
okHttpClient = OkHttpClient()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application)
categoryPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application) categoryPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application)
testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", "http://" + wikidataServer.hostName + ":" + wikidataServer.port + "/", sharedPreferences, categoryPreferences, Gson()) testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", "http://" + wikidataServer.hostName + ":" + wikidataServer.port + "/", sharedPreferences, categoryPreferences, Gson(), okHttpClient)
testObject.setWikiMediaToolforgeUrl("http://" + server.hostName + ":" + server.port + "/") testObject.setWikiMediaToolforgeUrl("http://" + server.hostName + ":" + server.port + "/")
} }