[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
This commit is contained in:
Vivek Maskara 2018-09-06 19:37:51 +05:30 committed by Josephine Lim
parent 54caad265a
commit 6eb01b8076
10 changed files with 182 additions and 171 deletions

View file

@ -1,7 +1,5 @@
package fr.free.nrw.commons.achievements;
import android.util.Log;
/**
* represnts Achievements class ans stores all the parameters
*/
@ -44,6 +42,23 @@ public class Achievements {
this.revertCount = revertCount;
}
/**
* Get Achievements object from FeedbackResponse
*
* @param response
* @return
*/
public static Achievements from(FeedbackResponse response) {
return new Achievements(response.getUniqueUsedImages(),
response.getArticlesUsingImages(),
response.getThanksReceived(),
response.getImagesEditedBySomeoneElse(),
response.getFeaturedImages().getQualityImages()
+ response.getFeaturedImages().getFeaturedPicturesOnWikimediaCommons(),
0,
response.getDeletedUploads());
}
/**
* Builder class for Achievements class
*/

View file

@ -2,16 +2,9 @@ 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;
@ -20,7 +13,6 @@ 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;
@ -34,13 +26,10 @@ 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 java.util.Objects;
import javax.inject.Inject;
@ -141,7 +130,6 @@ public class AchievementsActivity extends NavigationBaseActivity {
hideLayouts();
setAchievements();
setUploadCount();
setRevertCount();
initDrawer();
}
@ -201,61 +189,37 @@ public class AchievementsActivity extends NavigationBaseActivity {
* which then calls parseJson when results are fetched
*/
private void setAchievements() {
if(checkAccount()) {
compositeDisposable.add(mediaWikiApi
.getAchievements(sessionManager.getCurrentAccount().name)
.getAchievements(Objects.requireNonNull(sessionManager.getCurrentAccount()).name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
jsonObject -> parseJson(jsonObject),
response -> {
if (response != null) {
achievements = Achievements.from(response);
isRevertFetched = true;
isStatisticsFetched = true;
}
hideProgressBar();
},
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
.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
*/
private void setUploadCount() {
if(checkAccount()) {
compositeDisposable.add(mediaWikiApi
.getUploadCount(sessionManager.getCurrentAccount().name)
.getUploadCount(Objects.requireNonNull(sessionManager.getCurrentAccount()).name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
uploadCount -> setAchievementsUploadCount(uploadCount),
this::setAchievementsUploadCount,
t -> Timber.e(t, "Fetching upload count failed")
));
}
@ -293,33 +257,12 @@ public class AchievementsActivity extends NavigationBaseActivity {
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 ){
private void inflateAchievements(Achievements achievements) {
thanksReceived.setText(Integer.toString(achievements.getThanksReceived()));
imagesUsedByWikiProgessbar.setProgress
(100*achievements.getUniqueUsedImages()/levelInfo.getMaxUniqueImages() );
@ -393,17 +336,8 @@ public class AchievementsActivity extends NavigationBaseActivity {
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.setPositiveButton("Proceed", (dialog, which) -> shareScreen(screenshot));
alertadd.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel());
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

@ -11,7 +11,6 @@ import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
@ -22,6 +21,7 @@ import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
import okhttp3.OkHttpClient;
import static android.content.Context.MODE_PRIVATE;
import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.RECENT_SEARCH_AUTHORITY;

View file

@ -7,6 +7,8 @@ import android.support.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.File;
import javax.inject.Named;
import javax.inject.Singleton;
@ -15,6 +17,7 @@ import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import okhttp3.Cache;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
@ -25,8 +28,11 @@ public class NetworkingModule {
@Provides
@Singleton
public OkHttpClient provideOkHttpClient() {
return new OkHttpClient.Builder().build();
public OkHttpClient provideOkHttpClient(Context context) {
File dir = new File(context.getCacheDir(), "okHttpCache");
return new OkHttpClient.Builder()
.cache(new Cache(dir, OK_HTTP_CACHE_SIZE))
.build();
}
@Provides
@ -34,8 +40,9 @@ public class NetworkingModule {
public MediaWikiApi provideMediaWikiApi(Context context,
@Named("default_preferences") SharedPreferences defaultPreferences,
@Named("category_prefs") SharedPreferences categoryPrefs,
Gson gson) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultPreferences, categoryPrefs, gson);
Gson gson,
OkHttpClient okHttpClient) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultPreferences, categoryPrefs, gson, okHttpClient);
}
@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.CoreProtocolPNames;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
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.Media;
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.category.CategoryImageUtils;
import fr.free.nrw.commons.category.QueryContinue;
@ -76,14 +76,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private SharedPreferences defaultPreferences;
private SharedPreferences categoryPreferences;
private Gson gson;
private final OkHttpClient okHttpClient;
public ApacheHttpClientMediaWikiApi(Context context,
String apiURL,
String wikidatApiURL,
SharedPreferences defaultPreferences,
SharedPreferences categoryPreferences,
Gson gson) {
Gson gson,
OkHttpClient okHttpClient) {
this.context = context;
this.okHttpClient = okHttpClient;
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
@ -963,12 +966,11 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
/**
* 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
* @param userName MediaWiki user name
* @return
*/
@NonNull
@Override
public Single<JSONObject> getAchievements(String userName) {
public Single<FeedbackResponse> getAchievements(String userName) {
final String fetchAchievementUrlTemplate =
wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py";
return Single.fromCallable(() -> {
@ -982,46 +984,19 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
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;
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
return gson.fromJson(json, FeedbackResponse.class);
}
return null;
});
}
/**
* 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<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) {
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"));

View file

@ -4,13 +4,12 @@ import android.net.Uri;
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;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.notification.Notification;
import io.reactivex.Observable;
import io.reactivex.Single;
@ -98,13 +97,9 @@ public interface MediaWikiApi {
@NonNull
Single<Integer> getUploadCount(String userName);
@NonNull
Single<JSONObject> getRevertRespObjectSingle(String userName);
boolean isUserBlockedFromCommons();
@NonNull
Single<JSONObject> getAchievements(String userName);
Single<FeedbackResponse> getAchievements(String userName);
void logout();

View file

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

View file

@ -6,6 +6,7 @@ import android.preference.PreferenceManager
import com.google.gson.Gson
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.TestCommonsApplication
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
@ -30,14 +31,16 @@ class ApacheHttpClientMediaWikiApiTest {
private lateinit var wikidataServer: MockWebServer
private lateinit var sharedPreferences: SharedPreferences
private lateinit var categoryPreferences: SharedPreferences
private lateinit var okHttpClient: OkHttpClient
@Before
fun setUp() {
server = MockWebServer()
wikidataServer = MockWebServer()
okHttpClient = OkHttpClient()
sharedPreferences = 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 + "/")
}