From fa0370438b26b51fff8f0820632af0f25f42922f Mon Sep 17 00:00:00 2001 From: Devarsh Mavani Date: Sun, 20 Mar 2022 08:40:25 +0530 Subject: [PATCH] In app feedback (#4845) * rebase * Progress * Completed UI and POST Request * removed invalid string resource * Removed unused code & Added string resources * Resolved Code style issues * Javadoc for getters & setters * Codestyle fixes * Minor Fixes * wip * Tests * Comments * Fixed Tests * Minor changes * minor change * Comments * Minor Fixes * fixed tests * Removed Butterknife * Fixed tests * Removed Unecessary strings * Minor chnages * Minor fix * Minor changes * Minor changes * Implemented Suggestions * Removed Redundant Toast --- .../feedback/FeedbackContentCreator.java | 95 ++++++++++ .../nrw/commons/feedback/FeedbackDialog.java | 66 +++++++ .../feedback/OnFeedbackSubmitCallback.java | 15 ++ .../nrw/commons/feedback/model/Feedback.java | 171 ++++++++++++++++++ .../navtab/MoreBottomSheetFragment.java | 53 +++++- app/src/main/res/layout/dialog_feedback.xml | 136 ++++++++++++++ app/src/main/res/values-mk/strings.xml | 1 - app/src/main/res/values/strings.xml | 11 ++ .../FeedbackContentCreatorUnitTests.kt | 44 +++++ .../commons/feedback/FeedbackDialogTests.kt | 87 +++++++++ .../nrw/commons/feedback/FeedbackUnitTests.kt | 65 +++++++ .../MoreBottomSheetFragmentUnitTests.kt | 38 +++- 12 files changed, 777 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java create mode 100644 app/src/main/java/fr/free/nrw/commons/feedback/FeedbackDialog.java create mode 100644 app/src/main/java/fr/free/nrw/commons/feedback/OnFeedbackSubmitCallback.java create mode 100644 app/src/main/java/fr/free/nrw/commons/feedback/model/Feedback.java create mode 100644 app/src/main/res/layout/dialog_feedback.xml create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/feedback/FeedbackContentCreatorUnitTests.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/feedback/FeedbackDialogTests.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/feedback/FeedbackUnitTests.kt diff --git a/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java b/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java new file mode 100644 index 000000000..5b4b85093 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java @@ -0,0 +1,95 @@ +package fr.free.nrw.commons.feedback; + +import android.content.Context; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.feedback.model.Feedback; +import fr.free.nrw.commons.utils.LangCodeUtils; +import java.util.Locale; + +/** + * Creates a wikimedia recognizable format + * from feedback information + */ +public class FeedbackContentCreator { + private StringBuilder stringBuilder; + private Feedback feedback; + private Context context; + + public FeedbackContentCreator(Context context, Feedback feedback) { + this.feedback = feedback; + this.context = context; + init(); + } + + /** + * Initializes the string buffer object to append content from feedback object + */ + public void init() { + // Localization is not needed here, because this ends up on a page where developers read the feedback, so English is the most convenient. + + stringBuilder = new StringBuilder(); + stringBuilder.append("== "); + stringBuilder.append("Feedback from ~~~ for version "); + stringBuilder.append(feedback.getVersion()); + stringBuilder.append(" =="); + stringBuilder.append("\n"); + stringBuilder.append(feedback.getTitle()); + stringBuilder.append("\n"); + stringBuilder.append("\n"); + if (feedback.getApiLevel() != null) { + stringBuilder.append("* "); + stringBuilder.append(LangCodeUtils.getLocalizedResources(context, + Locale.ENGLISH).getString(R.string.api_level)); + stringBuilder.append(": "); + stringBuilder.append(feedback.getApiLevel()); + stringBuilder.append("\n"); + } + if (feedback.getAndroidVersion() != null) { + stringBuilder.append("* "); + stringBuilder.append(LangCodeUtils.getLocalizedResources(context, + Locale.ENGLISH).getString(R.string.android_version)); + stringBuilder.append(": "); + stringBuilder.append(feedback.getAndroidVersion()); + stringBuilder.append("\n"); + } + if (feedback.getDeviceManufacturer() != null) { + stringBuilder.append("* "); + stringBuilder.append(LangCodeUtils.getLocalizedResources(context, + Locale.ENGLISH).getString(R.string.device_manufacturer)); + stringBuilder.append(": "); + stringBuilder.append(feedback.getDeviceManufacturer()); + stringBuilder.append("\n"); + } + if (feedback.getDeviceModel() != null) { + stringBuilder.append("* "); + stringBuilder.append(LangCodeUtils.getLocalizedResources(context, + Locale.ENGLISH).getString(R.string.device_model)); + stringBuilder.append(": "); + stringBuilder.append(feedback.getDeviceModel()); + stringBuilder.append("\n"); + } + if (feedback.getDevice() != null) { + stringBuilder.append("* "); + stringBuilder.append(LangCodeUtils.getLocalizedResources(context, + Locale.ENGLISH).getString(R.string.device_name)); + stringBuilder.append(": "); + stringBuilder.append(feedback.getDevice()); + stringBuilder.append("\n"); + } + if (feedback.getNetworkType() != null) { + stringBuilder.append("* "); + stringBuilder.append(LangCodeUtils.getLocalizedResources(context, + Locale.ENGLISH).getString(R.string.network_type)); + stringBuilder.append(": "); + stringBuilder.append(feedback.getNetworkType()); + stringBuilder.append("\n"); + } + stringBuilder.append("~~~~"); + stringBuilder.append("\n"); + } + + @Override + public String toString() { + return stringBuilder.toString(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackDialog.java b/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackDialog.java new file mode 100644 index 000000000..fb4893ea3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackDialog.java @@ -0,0 +1,66 @@ +package fr.free.nrw.commons.feedback; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Toast; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.databinding.DialogFeedbackBinding; +import fr.free.nrw.commons.feedback.model.Feedback; +import fr.free.nrw.commons.utils.ConfigUtils; +import fr.free.nrw.commons.utils.DeviceInfoUtil; + +/** + * Feedback dialog that asks user for message and + * other device specifications + */ +public class FeedbackDialog extends Dialog { + DialogFeedbackBinding dialogFeedbackBinding; + + private OnFeedbackSubmitCallback onFeedbackSubmitCallback; + + public FeedbackDialog(Context context, OnFeedbackSubmitCallback onFeedbackSubmitCallback) { + super(context); + this.onFeedbackSubmitCallback = onFeedbackSubmitCallback; + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + dialogFeedbackBinding = DialogFeedbackBinding.inflate(getLayoutInflater()); + final View view = dialogFeedbackBinding.getRoot(); + setContentView(view); + dialogFeedbackBinding.btnSubmitFeedback.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + submitFeedback(); + } + }); + } + + /** + * When the button is clicked, it will create a feedback object + * and give a callback to calling activity/fragment + */ + void submitFeedback() { + if(dialogFeedbackBinding.feedbackItemEditText.getText().toString().equals("")) { + dialogFeedbackBinding.feedbackItemEditText.setError(getContext().getString(R.string.enter_description)); + return; + } + String appVersion = ConfigUtils.getVersionNameWithSha(getContext()); + String androidVersion = dialogFeedbackBinding.androidVersionCheckbox.isChecked() ? DeviceInfoUtil.getAndroidVersion() : null; + String apiLevel = dialogFeedbackBinding.apiLevelCheckbox.isChecked() ? DeviceInfoUtil.getAPILevel() : null; + String deviceManufacturer = dialogFeedbackBinding.deviceManufacturerCheckbox.isChecked() ? DeviceInfoUtil.getDeviceManufacturer() : null; + String deviceModel = dialogFeedbackBinding.deviceModelCheckbox.isChecked() ? DeviceInfoUtil.getDeviceModel() : null; + String deviceName = dialogFeedbackBinding.deviceNameCheckbox.isChecked() ? DeviceInfoUtil.getDevice() : null; + String networkType = dialogFeedbackBinding.networkTypeCheckbox.isChecked() ? DeviceInfoUtil.getConnectionType(getContext()).toString() : null; + Feedback feedback = new Feedback(appVersion, apiLevel + , dialogFeedbackBinding.feedbackItemEditText.getText().toString() + , androidVersion, deviceModel, deviceManufacturer, deviceName, networkType); + onFeedbackSubmitCallback.onFeedbackSubmit(feedback); + dismiss(); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/feedback/OnFeedbackSubmitCallback.java b/app/src/main/java/fr/free/nrw/commons/feedback/OnFeedbackSubmitCallback.java new file mode 100644 index 000000000..0d695061a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/feedback/OnFeedbackSubmitCallback.java @@ -0,0 +1,15 @@ +package fr.free.nrw.commons.feedback; + +import fr.free.nrw.commons.feedback.model.Feedback; + +/** + * This interface is used to provide callback + * from Feedback dialog whenever submit button is clicked + */ +public interface OnFeedbackSubmitCallback { + + /** + * callback function, called when user clicks on submit + */ + void onFeedbackSubmit(Feedback feedback); +} diff --git a/app/src/main/java/fr/free/nrw/commons/feedback/model/Feedback.java b/app/src/main/java/fr/free/nrw/commons/feedback/model/Feedback.java new file mode 100644 index 000000000..6e3a8cb0f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/feedback/model/Feedback.java @@ -0,0 +1,171 @@ +package fr.free.nrw.commons.feedback.model; + +/** + * Pojo class for storing information that are required while uploading a feedback + */ +public class Feedback { + /** + * Version of app + */ + private String version; + /** + * API level of user's phone + */ + private String apiLevel; + /** + * Title/Description entered by user + */ + private String title; + /** + * Android version of user's device + */ + private String androidVersion; + /** + * Device Model of user's device + */ + private String deviceModel; + /** + * Device manufacturer name + */ + private String deviceManufacturer; + /** + * Device name stored on user's device + */ + private String device; + /** + * network type user is having (Ex: Wifi) + */ + private String networkType; + + public Feedback(final String version, final String apiLevel, final String title, final String androidVersion, + final String deviceModel, final String deviceManufacturer, final String device, final String networkType + ) { + this.version = version; + this.apiLevel = apiLevel; + this.title = title; + this.androidVersion = androidVersion; + this.deviceModel = deviceModel; + this.deviceManufacturer = deviceManufacturer; + this.device = device; + this.networkType = networkType; + } + + /** + * Get the version from which this piece of feedback is being sent. + * Ex: 3.0.1 + */ + public String getVersion() { + return version; + } + + /** + * Set the version of app to given version + */ + public void setVersion(final String version) { + this.version = version; + } + + /** + * gets api level of device + * Ex: 28 + */ + public String getApiLevel() { + return apiLevel; + } + + /** + * sets api level value to given value + */ + public void setApiLevel(final String apiLevel) { + this.apiLevel = apiLevel; + } + + /** + * gets feedback text entered by user + */ + public String getTitle() { + return title; + } + + /** + * sets feedback text + */ + public void setTitle(final String title) { + this.title = title; + } + + /** + * gets android version of device + * Ex: 9 + */ + public String getAndroidVersion() { + return androidVersion; + } + + /** + * sets value of android version + */ + public void setAndroidVersion(final String androidVersion) { + this.androidVersion = androidVersion; + } + + /** + * get device model of current device + * Ex: Redmi 6 Pro + */ + public String getDeviceModel() { + return deviceModel; + } + + /** + * sets value of device model to a given value + */ + public void setDeviceModel(final String deviceModel) { + this.deviceModel = deviceModel; + } + + /** + * get device manufacturer of user's device + * Ex: Redmi + */ + public String getDeviceManufacturer() { + return deviceManufacturer; + } + + /** + * set device manufacturer value to a given value + */ + public void setDeviceManufacturer(final String deviceManufacturer) { + this.deviceManufacturer = deviceManufacturer; + } + + /** + * get device name of user's device + */ + public String getDevice() { + return device; + } + + /** + * sets device name value to a given value + */ + public void setDevice(final String device) { + this.device = device; + } + + /** + * get network type of user's network + * Ex: wifi + */ + public String getNetworkType() { + return networkType; + } + + /** + * sets network type to a given value + */ + public void setNetworkType(final String networkType) { + this.networkType = networkType; + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java index f54ba61c7..7de7e15e2 100644 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java @@ -24,13 +24,23 @@ 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.actions.PageEditClient; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.feedback.FeedbackContentCreator; +import fr.free.nrw.commons.feedback.model.Feedback; +import fr.free.nrw.commons.feedback.FeedbackDialog; +import fr.free.nrw.commons.feedback.OnFeedbackSubmitCallback; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.logging.CommonsLogSender; import fr.free.nrw.commons.profile.ProfileActivity; import fr.free.nrw.commons.review.ReviewActivity; import fr.free.nrw.commons.settings.SettingsActivity; +import io.reactivex.Single; +import io.reactivex.SingleSource; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import java.util.concurrent.Callable; import javax.inject.Inject; import javax.inject.Named; import timber.log.Timber; @@ -47,6 +57,10 @@ public class MoreBottomSheetFragment extends BottomSheetDialogFragment { @Inject @Named("default_preferences") JsonKvStore store; + @Inject + @Named("commons-page-edit") + PageEditClient pageEditClient; + @Nullable @Override public View onCreateView(@NonNull final LayoutInflater inflater, @@ -104,7 +118,44 @@ public class MoreBottomSheetFragment extends BottomSheetDialogFragment { @OnClick(R.id.more_feedback) public void onFeedbackClicked() { - showAlertDialog(); + showFeedbackDialog(); + } + + /** + * Creates and shows a dialog asking feedback from users + */ + private void showFeedbackDialog() { + new FeedbackDialog(getContext(), new OnFeedbackSubmitCallback() { + @Override + public void onFeedbackSubmit(Feedback feedback) { + uploadFeedback(feedback); + } + }).show(); + } + + /** + * uploads feedback data on the server + */ + void uploadFeedback(Feedback feedback) { + FeedbackContentCreator feedbackContentCreator = new FeedbackContentCreator(getContext(), feedback); + + Single single = + pageEditClient.prependEdit("Commons:Mobile_app/Feedback", feedbackContentCreator.toString(), "Summary") + .flatMapSingle(result -> Single.just(result)) + .firstOrError(); + + Single.defer((Callable>) () -> single) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(aBoolean -> { + if (aBoolean) { + Toast.makeText(getContext(), getString(R.string.thanks_feedback), Toast.LENGTH_SHORT) + .show(); + } else { + Toast.makeText(getContext(), getString(R.string.error_feedback), + Toast.LENGTH_SHORT).show(); + } + }); } /** diff --git a/app/src/main/res/layout/dialog_feedback.xml b/app/src/main/res/layout/dialog_feedback.xml new file mode 100644 index 000000000..9acef11bd --- /dev/null +++ b/app/src/main/res/layout/dialog_feedback.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +