From 6ea3198d65626b86d976dd84f8ae4aa38600d598 Mon Sep 17 00:00:00 2001 From: RISHAV GUPTA Date: Sun, 20 Mar 2022 07:50:25 +0530 Subject: [PATCH 001/721] Fixes : #4273 Add link to user guide in About (#4882) * user guide added in about * test added * unit test added --- .../java/fr/free/nrw/commons/AboutActivityTest.kt | 9 +++++++++ app/src/main/java/fr/free/nrw/commons/AboutActivity.java | 6 ++++++ app/src/main/java/fr/free/nrw/commons/Urls.kt | 1 + app/src/main/res/layout/activity_about.xml | 9 +++++++++ app/src/main/res/values/strings.xml | 1 + .../kotlin/fr/free/nrw/commons/AboutActivityUnitTests.kt | 6 ++++++ 6 files changed, 32 insertions(+) diff --git a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt index 56e30f2d1..0e71235d0 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt +++ b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt @@ -104,6 +104,15 @@ class AboutActivityTest { IntentMatchers.hasData(Urls.CREDITS_URL))) } + @Test + @Ignore("Fix Failing Test") + fun testLaunchUserGuide() { + Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click()) + Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW), + IntentMatchers.hasData(Urls.USER_GUIDE_URL))) + } + + @Test @Ignore("Fix Failing Test") fun testLaunchAboutFaq() { diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java index f75692e5a..c9f8bc565 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java @@ -64,6 +64,7 @@ public class AboutActivity extends BaseActivity { Utils.setUnderlinedText(binding.aboutFaq, R.string.about_faq, getApplicationContext()); Utils.setUnderlinedText(binding.aboutRateUs, R.string.about_rate_us, getApplicationContext()); + Utils.setUnderlinedText(binding.aboutUserGuide, R.string.user_guide, getApplicationContext()); Utils.setUnderlinedText(binding.aboutPrivacyPolicy, R.string.about_privacy_policy, getApplicationContext()); Utils.setUnderlinedText(binding.aboutTranslate, R.string.about_translate, getApplicationContext()); Utils.setUnderlinedText(binding.aboutCredits, R.string.about_credits, getApplicationContext()); @@ -77,6 +78,7 @@ public class AboutActivity extends BaseActivity { binding.aboutRateUs.setOnClickListener(this::launchRatings); binding.aboutCredits.setOnClickListener(this::launchCredits); binding.aboutPrivacyPolicy.setOnClickListener(this::launchPrivacyPolicy); + binding.aboutUserGuide.setOnClickListener(this::launchUserGuide); binding.aboutFaq.setOnClickListener(this::launchFrequentlyAskedQuesions); binding.aboutTranslate.setOnClickListener(this::launchTranslate); } @@ -114,6 +116,10 @@ public class AboutActivity extends BaseActivity { Utils.handleWebUrl(this, Uri.parse(Urls.CREDITS_URL)); } + public void launchUserGuide(View view) { + Utils.handleWebUrl(this, Uri.parse(Urls.USER_GUIDE_URL)); + } + public void launchPrivacyPolicy(View view) { Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)); } diff --git a/app/src/main/java/fr/free/nrw/commons/Urls.kt b/app/src/main/java/fr/free/nrw/commons/Urls.kt index 88470bc69..def86da14 100644 --- a/app/src/main/java/fr/free/nrw/commons/Urls.kt +++ b/app/src/main/java/fr/free/nrw/commons/Urls.kt @@ -5,6 +5,7 @@ internal object Urls { const val GITHUB_REPO_URL = "https://github.com/commons-app/apps-android-commons" const val WEBSITE_URL = "https://commons-app.github.io" const val CREDITS_URL = "https://github.com/commons-app/apps-android-commons/blob/master/CREDITS" + const val USER_GUIDE_URL = "https://commons-app.github.io/docs.html" const val FAQ_URL = "https://github.com/commons-app/commons-app-documentation/blob/master/android/Frequently-Asked-Questions.md" const val PLAY_STORE_PREFIX = "market://details?id=" const val PLAY_STORE_URL_PREFIX = "https://play.google.com/store/apps/details?id=" diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index 3fbdb4cab..c509dad71 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -120,6 +120,15 @@ android:textColor="?attr/colorAccent" /> + + Commons Rate us FAQ + User Guide Skip Tutorial Internet unavailable Error fetching notifications diff --git a/app/src/test/kotlin/fr/free/nrw/commons/AboutActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/AboutActivityUnitTests.kt index fd92436fb..806d72df9 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/AboutActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/AboutActivityUnitTests.kt @@ -83,6 +83,12 @@ class AboutActivityUnitTests { activity.launchPrivacyPolicy(null) } + @Test + @Throws(Exception::class) + fun testLaunchUserGuide() { + activity.launchUserGuide(null) + } + @Test @Throws(Exception::class) fun testLaunchFrequentlyAskedQuestions() { From 00760ba1c6eb2d9676237827686d37c9b7e45cb8 Mon Sep 17 00:00:00 2001 From: RISHAV GUPTA Date: Sun, 20 Mar 2022 07:51:54 +0530 Subject: [PATCH 002/721] Fixes [Bug]: Prod/beta flavor messages may be nonsense for users #4850 (#4899) * small string changes * small change done * small changes done --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ebbaf10d1..de07eac2e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -690,7 +690,7 @@ Upload your first media by tapping on the add button. How about adding the place where this image was taken?\nLocation data helps Wiki editors find your picture, making it much more useful.\nThank you! Add location Please remove from this email any information that you are not comfortable sharing publicly. Also, please be aware that your email address with which you are posting, and the associated name and profile picture, will be visible publicly. - Achievements are only available in the prod flavor. - The leaderboard is only available in the prod flavor. + Achievements are only available in the prod flavor, please check the developer documentation. + The leaderboard is only available in the prod flavor, please check the developer documentation. Please only upload pictures you have taken by yourself. Uploaders of copyrighted images will be blocked. This applies to the beta flavor too. Thank you for testing the app! From fa0370438b26b51fff8f0820632af0f25f42922f Mon Sep 17 00:00:00 2001 From: Devarsh Mavani Date: Sun, 20 Mar 2022 08:40:25 +0530 Subject: [PATCH 003/721] 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +