From 1f49480e8c66f9c1770eb09599ecc8be29f0f89c Mon Sep 17 00:00:00 2001 From: Paras Date: Tue, 31 Mar 2026 11:52:14 +0530 Subject: [PATCH 01/12] test: add MQTT settings tests (defaults, persistence, validation) --- .../catroid/test/mqtt/MqttSettingsTest.kt | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt diff --git a/catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt new file mode 100644 index 00000000000..624dfc0f9bf --- /dev/null +++ b/catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt @@ -0,0 +1,220 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2025 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.test.mqtt + +import android.os.Build +import android.preference.PreferenceManager +import org.catrobat.catroid.ui.settingsfragments.MqttSettingsFragment +import org.catrobat.catroid.ui.settingsfragments.SettingsFragment +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.P], instrumentedPackages = []) +class MqttSettingsTest { + + private val context get() = RuntimeEnvironment.getApplication() + + @Before + fun setUp() { + PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit() + } + + // --- Default value tests --- + + @Test + fun testMqttEnabledDefaultIsFalse() { + assertFalse(SettingsFragment.isMqttSharedPreferenceEnabled(context)) + } + + @Test + fun testMqttHostDefaultIs192168_0_1() { + assertEquals("192.168.0.1", SettingsFragment.getMqttHost(context)) + } + + @Test + fun testMqttPortDefaultIs1883() { + assertEquals(1883, SettingsFragment.getMqttPort(context)) + } + + @Test + fun testMqttTlsEnabledDefaultIsFalse() { + assertFalse(SettingsFragment.isMqttTlsEnabled(context)) + } + + @Test + fun testMqttUsernameDefaultIsEmpty() { + assertEquals("", SettingsFragment.getMqttUsername(context)) + } + + @Test + fun testMqttPasswordDefaultIsEmpty() { + assertEquals("", SettingsFragment.getMqttPassword(context)) + } + + @Test + fun testMqttClientIdDefaultIsEmpty() { + assertEquals("", SettingsFragment.getMqttClientId(context)) + } + + // --- Persistence tests --- + + @Test + fun testMqttEnabledPersistsAfterWrite() { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putBoolean(SettingsFragment.SETTINGS_SHOW_MQTT_BRICKS, true).commit() + assertTrue(SettingsFragment.isMqttSharedPreferenceEnabled(context)) + } + + @Test + fun testMqttHostPersistsAfterWrite() { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(SettingsFragment.MQTT_HOST, "broker.hivemq.com").commit() + assertEquals("broker.hivemq.com", SettingsFragment.getMqttHost(context)) + } + + @Test + fun testMqttPortPersistsAfterWrite() { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(SettingsFragment.MQTT_PORT, "8883").commit() + assertEquals(8883, SettingsFragment.getMqttPort(context)) + } + + @Test + fun testMqttTlsPersistsAfterWrite() { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putBoolean(SettingsFragment.MQTT_TLS, true).commit() + assertTrue(SettingsFragment.isMqttTlsEnabled(context)) + } + + @Test + fun testMqttUsernamePersistsAfterWrite() { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(SettingsFragment.MQTT_USERNAME, "testuser").commit() + assertEquals("testuser", SettingsFragment.getMqttUsername(context)) + } + + @Test + fun testMqttPasswordPersistsAfterWrite() { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(SettingsFragment.MQTT_PASSWORD, "secret").commit() + assertEquals("secret", SettingsFragment.getMqttPassword(context)) + } + + @Test + fun testMqttClientIdPersistsAfterWrite() { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(SettingsFragment.MQTT_CLIENT_ID, "device-001").commit() + assertEquals("device-001", SettingsFragment.getMqttClientId(context)) + } + + // --- getMqttPort fallback tests --- + + @Test + fun testMqttPortFallbackOnNonIntegerValue() { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(SettingsFragment.MQTT_PORT, "abc").commit() + assertEquals(1883, SettingsFragment.getMqttPort(context)) + } + + @Test + fun testMqttPortFallbackOnEmptyValue() { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(SettingsFragment.MQTT_PORT, "").commit() + assertEquals(1883, SettingsFragment.getMqttPort(context)) + } + + @Test + fun testMqttPortFallbackOnFloatValue() { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(SettingsFragment.MQTT_PORT, "1883.5").commit() + assertEquals(1883, SettingsFragment.getMqttPort(context)) + } + + // --- Port validation: boundary tests --- + + @Test + fun testPortValidationRejectsZero() { + assertFalse(MqttSettingsFragment.isValidPort("0")) + } + + @Test + fun testPortValidationRejectsNegativeNumber() { + assertFalse(MqttSettingsFragment.isValidPort("-1")) + } + + @Test + fun testPortValidationRejects65536() { + assertFalse(MqttSettingsFragment.isValidPort("65536")) + } + + @Test + fun testPortValidationAcceptsMinBoundary() { + assertTrue(MqttSettingsFragment.isValidPort("1")) + } + + @Test + fun testPortValidationAccepts1883() { + assertTrue(MqttSettingsFragment.isValidPort("1883")) + } + + @Test + fun testPortValidationAcceptsMaxBoundary() { + assertTrue(MqttSettingsFragment.isValidPort("65535")) + } + + // --- Port validation: non-numeric input --- + + @Test + fun testPortValidationRejectsAlphabeticString() { + assertFalse(MqttSettingsFragment.isValidPort("abc")) + } + + @Test + fun testPortValidationRejectsEmptyString() { + assertFalse(MqttSettingsFragment.isValidPort("")) + } + + @Test + fun testPortValidationRejectsPartialNumber() { + assertFalse(MqttSettingsFragment.isValidPort("1883abc")) + } + + @Test + fun testPortValidationRejectsWhitespace() { + assertFalse(MqttSettingsFragment.isValidPort(" 1883 ")) + } + + @Test + fun testPortValidationRejectsFloatString() { + assertFalse(MqttSettingsFragment.isValidPort("1883.5")) + } +} From 3ce2096265f81e31a2511fca76d95b1eb1cb1e4b Mon Sep 17 00:00:00 2001 From: Paras Date: Tue, 31 Mar 2026 11:58:35 +0530 Subject: [PATCH 02/12] feat: add MQTT settings constants and accessor methods --- .../settingsfragments/SettingsFragment.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java index 46613104114..27053f6631f 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java @@ -131,6 +131,16 @@ public class SettingsFragment extends PreferenceFragment { public static final String RASPI_PORT = "setting_raspi_port_preference"; public static final String RASPI_VERSION_SPINNER = "setting_raspi_version_preference"; + public static final String MQTT_SCREEN_KEY = "settings_mqtt_screen"; + public static final String SETTINGS_SHOW_MQTT_BRICKS = "setting_mqtt_bricks"; + public static final String MQTT_CONNECTION_SETTINGS_CATEGORY = "setting_mqtt_category"; + public static final String MQTT_HOST = "setting_mqtt_host"; + public static final String MQTT_PORT = "setting_mqtt_port"; + public static final String MQTT_TLS = "setting_mqtt_tls"; + public static final String MQTT_USERNAME = "setting_mqtt_username"; + public static final String MQTT_PASSWORD = "setting_mqtt_password"; + public static final String MQTT_CLIENT_ID = "setting_mqtt_client_id"; + public static final String SETTINGS_CRASH_REPORTS = "setting_enable_crash_reports"; public static final String TAG = SettingsFragment.class.getSimpleName(); @@ -408,6 +418,38 @@ public static boolean isRaspiSharedPreferenceEnabled(Context context) { return getBooleanSharedPreference(false, SETTINGS_SHOW_RASPI_BRICKS, context); } + public static boolean isMqttSharedPreferenceEnabled(Context context) { + return getBooleanSharedPreference(false, SETTINGS_SHOW_MQTT_BRICKS, context); + } + + public static String getMqttHost(Context context) { + return getSharedPreferences(context).getString(MQTT_HOST, "192.168.0.1"); + } + + public static int getMqttPort(Context context) { + try { + return Integer.parseInt(getSharedPreferences(context).getString(MQTT_PORT, "1883")); + } catch (NumberFormatException e) { + return 1883; + } + } + + public static boolean isMqttTlsEnabled(Context context) { + return getBooleanSharedPreference(false, MQTT_TLS, context); + } + + public static String getMqttUsername(Context context) { + return getSharedPreferences(context).getString(MQTT_USERNAME, ""); + } + + public static String getMqttPassword(Context context) { + return getSharedPreferences(context).getString(MQTT_PASSWORD, ""); + } + + public static String getMqttClientId(Context context) { + return getSharedPreferences(context).getString(MQTT_CLIENT_ID, ""); + } + public static boolean isNfcSharedPreferenceEnabled(Context context) { return getBooleanSharedPreference(false, SETTINGS_SHOW_NFC_BRICKS, context); } From 638bd5459e74ddbbac316a886926503756cb4d78 Mon Sep 17 00:00:00 2001 From: Paras Date: Tue, 31 Mar 2026 12:01:50 +0530 Subject: [PATCH 03/12] feat: add MQTT settings fragment with port validation logic --- .../settingsfragments/MqttSettingsFragment.kt | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt new file mode 100644 index 00000000000..193f987f8a6 --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt @@ -0,0 +1,103 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2025 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.ui.settingsfragments + +import android.os.Bundle +import android.preference.CheckBoxPreference +import android.preference.EditTextPreference +import android.preference.Preference +import android.preference.PreferenceCategory +import android.preference.PreferenceFragment +import android.widget.Toast +import androidx.annotation.Nullable +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AppCompatActivity +import org.catrobat.catroid.R +import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_CONNECTION_SETTINGS_CATEGORY +import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_CLIENT_ID +import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_HOST +import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_PASSWORD +import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_PORT +import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_USERNAME +import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.SETTINGS_SHOW_MQTT_BRICKS + +class MqttSettingsFragment : PreferenceFragment() { + + override fun onResume() { + super.onResume() + (activity as AppCompatActivity).supportActionBar?.title = preferenceScreen.title + } + + override fun onActivityCreated(@Nullable savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + SettingsFragment.setToChosenLanguage(activity) + addPreferencesFromResource(R.xml.mqtt_preferences) + + val mqttCheckBoxPreference = findPreference(SETTINGS_SHOW_MQTT_BRICKS) as CheckBoxPreference + val mqttConnectionSettings = findPreference(MQTT_CONNECTION_SETTINGS_CATEGORY) as PreferenceCategory + mqttConnectionSettings.isEnabled = mqttCheckBoxPreference.isChecked + + mqttCheckBoxPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, isChecked -> + mqttConnectionSettings.isEnabled = isChecked as Boolean + true + } + + bindSummaryToValue(findPreference(MQTT_HOST) as EditTextPreference) + bindSummaryToValue(findPreference(MQTT_USERNAME) as EditTextPreference) + bindSummaryToValue(findPreference(MQTT_CLIENT_ID) as EditTextPreference) + + val portPreference = findPreference(MQTT_PORT) as EditTextPreference + portPreference.summary = portPreference.text + portPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + if (!isValidPort(newValue.toString())) { + Toast.makeText(activity, R.string.preference_mqtt_port_invalid, Toast.LENGTH_SHORT).show() + return@OnPreferenceChangeListener false + } + portPreference.summary = newValue.toString() + true + } + + val passwordPreference = findPreference(MQTT_PASSWORD) as EditTextPreference + passwordPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> true } + } + + private fun bindSummaryToValue(preference: EditTextPreference) { + preference.summary = preference.text + preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { pref, newValue -> + (pref as EditTextPreference).summary = newValue.toString() + true + } + } + + companion object { + @JvmField val TAG: String = MqttSettingsFragment::class.java.simpleName + + @VisibleForTesting + @JvmStatic + fun isValidPort(value: String): Boolean { + val port = value.toIntOrNull() ?: return false + return port in 1..65535 + } + } +} From 4995ba800aac09de6d3a1c0f5d05b60d3017aa10 Mon Sep 17 00:00:00 2001 From: Paras Date: Tue, 31 Mar 2026 12:02:10 +0530 Subject: [PATCH 04/12] feat: add MQTT settings UI resources and preferences --- catroid/src/main/res/values/strings.xml | 17 +++++ catroid/src/main/res/xml/mqtt_preferences.xml | 76 +++++++++++++++++++ catroid/src/main/res/xml/preferences.xml | 5 ++ 3 files changed, 98 insertions(+) create mode 100644 catroid/src/main/res/xml/mqtt_preferences.xml diff --git a/catroid/src/main/res/values/strings.xml b/catroid/src/main/res/values/strings.xml index 2beb2fed14e..3f8ea8b5104 100644 --- a/catroid/src/main/res/values/strings.xml +++ b/catroid/src/main/res/values/strings.xml @@ -1531,6 +1531,23 @@ needs read and write access to it. You can always change permissions through you Error: Could not connect to %s : %d + + MQTT extension + Allow the app to connect to MQTT brokers + Enable MQTT bricks + Enable bricks and sensors for MQTT + MQTT settings + Broker host + IP address or hostname of the MQTT broker + Broker port + Port number of the MQTT broker (1–65535) + Use TLS + Username + Password + Client ID + Port must be a number between 1 and 65535 + + Set NXT motor to diff --git a/catroid/src/main/res/xml/mqtt_preferences.xml b/catroid/src/main/res/xml/mqtt_preferences.xml new file mode 100644 index 00000000000..4d51c1fb3bd --- /dev/null +++ b/catroid/src/main/res/xml/mqtt_preferences.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/catroid/src/main/res/xml/preferences.xml b/catroid/src/main/res/xml/preferences.xml index 91c6163f3d3..7e91808aaba 100644 --- a/catroid/src/main/res/xml/preferences.xml +++ b/catroid/src/main/res/xml/preferences.xml @@ -104,6 +104,11 @@ android:summary="@string/preference_description_raspi_bricks" android:title="@string/preference_title_enable_raspi_bricks" /> + + Date: Tue, 31 Mar 2026 12:02:35 +0530 Subject: [PATCH 05/12] feat: integrate MQTT settings screen into SettingsFragment navigation --- .../catroid/ui/settingsfragments/SettingsFragment.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java index 27053f6631f..00a530b53fc 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java @@ -272,6 +272,12 @@ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preferen .addToBackStack(RaspberryPiSettingsFragment.TAG) .commit(); break; + case MQTT_SCREEN_KEY: + getFragmentManager().beginTransaction() + .replace(R.id.content_frame, new MqttSettingsFragment(), MqttSettingsFragment.TAG) + .addToBackStack(MqttSettingsFragment.TAG) + .commit(); + break; } return super.onPreferenceTreeClick(preferenceScreen, preference); } From cc34243c9aeec036ac237e4add7d19647630227a Mon Sep 17 00:00:00 2001 From: Paras Date: Tue, 31 Mar 2026 12:02:42 +0530 Subject: [PATCH 06/12] test: add UI test for MQTT settings entry --- .../uiespresso/ui/activity/SettingsFragmentTest.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/SettingsFragmentTest.java b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/SettingsFragmentTest.java index ca6c7d435bd..7d10b99e5e0 100644 --- a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/SettingsFragmentTest.java +++ b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/SettingsFragmentTest.java @@ -85,6 +85,7 @@ import static org.catrobat.catroid.ui.settingsfragments.SettingsFragment.SETTINGS_SHOW_PHIRO_BRICKS_CHECKBOX_PREFERENCE; import static org.catrobat.catroid.ui.settingsfragments.SettingsFragment.SETTINGS_SHOW_PLOT_BRICKS; import static org.catrobat.catroid.ui.settingsfragments.SettingsFragment.SETTINGS_SHOW_RASPI_BRICKS; +import static org.catrobat.catroid.ui.settingsfragments.SettingsFragment.SETTINGS_SHOW_MQTT_BRICKS; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.core.AllOf.allOf; @@ -123,7 +124,7 @@ public class SettingsFragmentTest { SETTINGS_CAST_GLOBALLY_ENABLED, SETTINGS_SHOW_AI_SPEECH_RECOGNITION_SENSORS, SETTINGS_SHOW_AI_SPEECH_SYNTHETIZATION_SENSORS, SETTINGS_SHOW_AI_FACE_DETECTION_SENSORS, SETTINGS_SHOW_AI_POSE_DETECTION_SENSORS, - SETTINGS_SHOW_AI_TEXT_RECOGNITION_SENSORS)); + SETTINGS_SHOW_AI_TEXT_RECOGNITION_SENSORS, SETTINGS_SHOW_MQTT_BRICKS)); private Map initialSettings = new HashMap<>(); private Matcher expectedBrowserIntent; @@ -258,6 +259,15 @@ public void rasPiSettingsTest() { checkPreference(R.string.preference_title_enable_raspi_bricks, SETTINGS_SHOW_RASPI_BRICKS); } + @Category({Cat.AppUi.class, Level.Smoke.class, Cat.Gadgets.class}) + @Test + public void mqttSettingsTest() { + onData(PreferenceMatchers.withTitle(R.string.preference_title_enable_mqtt_bricks)) + .perform(click()); + + checkPreference(R.string.preference_title_enable_mqtt_bricks, SETTINGS_SHOW_MQTT_BRICKS); + } + @Category({Cat.AppUi.class, Level.Smoke.class, Cat.Gadgets.class}) @Test public void aiSettingsTest() { From ad8a53eab14cbcee2833a0eb1d27a8ab59017813 Mon Sep 17 00:00:00 2001 From: Paras Date: Sun, 21 Jun 2026 13:54:13 +0530 Subject: [PATCH 07/12] CATROID-1670 Add Eclipse Paho MQTT dependencies and Android config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the Eclipse Paho MQTT Java library dependency to the Catroid module's build configuration. This lays the groundwork for the connection manager and the new MQTT bricks. - Declare org.eclipse.paho.client.mqttv3:1.2.5 via Maven Central (Eclipse Paho repo not required) - Declare org.eclipse.paho.android.service:1.1.1 via Maven Central - Add FEATURE_MQTT_ENABLED build config flag in build.gradle - Register MqttService in AndroidManifest.xml - Add MqttDependencySanityTest to verify Paho classes are accessible on the test classpath INTERNET and ACCESS_NETWORK_STATE permissions were already present in the manifest — no changes required. --- catroid/build.gradle | 5 +++ catroid/src/main/AndroidManifest.xml | 3 +- .../test/mqtt/MqttDependencySanityTest.kt | 44 +++++++++++++++++++ .../catroid/test/mqtt/MqttSettingsTest.kt | 2 +- 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttDependencySanityTest.kt diff --git a/catroid/build.gradle b/catroid/build.gradle index 6b0ce477584..e076e6ebca5 100644 --- a/catroid/build.gradle +++ b/catroid/build.gradle @@ -154,6 +154,7 @@ android { buildConfigField "boolean", "FEATURE_NFC_ENABLED", "true" buildConfigField "boolean", "FEATURE_POCKETMUSIC_ENABLED", "true" buildConfigField "boolean", "FEATURE_RASPI_ENABLED", "true" + buildConfigField "boolean", "FEATURE_MQTT_ENABLED", "true" buildConfigField "boolean", "FEATURE_SCRATCH_CONVERTER_ENABLED", "true" buildConfigField "boolean", "FEATURE_USER_REPORTERS_ENABLED", "true" buildConfigField "boolean", "FEATURE_MULTIPLAYER_VARIABLES_ENABLED", "true" @@ -420,6 +421,10 @@ dependencies { implementation 'com.google.guava:guava:28.2-android' implementation 'com.google.code.gson:gson:2.8.7' + // MQTT + implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' + implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1' + implementation 'com.koushikdutta.async:androidasync:2.2.1' implementation 'com.squareup.picasso:picasso:2.71828' implementation 'ar.com.hjg:pngj:2.1.0' diff --git a/catroid/src/main/AndroidManifest.xml b/catroid/src/main/AndroidManifest.xml index a7a593a8f32..391a9171ff2 100644 --- a/catroid/src/main/AndroidManifest.xml +++ b/catroid/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ From 145c0a82e66c2df2db199b716af3742d6d4f97d1 Mon Sep 17 00:00:00 2001 From: Paras Date: Fri, 10 Jul 2026 05:58:20 +0530 Subject: [PATCH 11/12] Fix MQTT settings: correct Espresso test, remove redundant listener, encrypt password storage - Fix mqttSettingsTest() to use preference_title_mqtt_enabled instead of preference_title_enable_mqtt_bricks so Espresso finds the actual checkbox inside MqttSettingsFragment - Remove redundant password onPreferenceChangeListener that only returned true - Store MQTT password in EncryptedSharedPreferences (AES256) instead of plaintext default SharedPreferences --- .../ui/activity/SettingsFragmentTest.java | 2 +- .../settingsfragments/MqttSettingsFragment.kt | 23 ++++++++++++++++++- .../settingsfragments/SettingsFragment.java | 16 ++++++++++++- .../catroid/test/mqtt/MqttSettingsTest.kt | 12 +++++++--- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/SettingsFragmentTest.java b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/SettingsFragmentTest.java index 7d10b99e5e0..3a8ca01fcb8 100644 --- a/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/SettingsFragmentTest.java +++ b/catroid/src/androidTest/java/org/catrobat/catroid/uiespresso/ui/activity/SettingsFragmentTest.java @@ -265,7 +265,7 @@ public void mqttSettingsTest() { onData(PreferenceMatchers.withTitle(R.string.preference_title_enable_mqtt_bricks)) .perform(click()); - checkPreference(R.string.preference_title_enable_mqtt_bricks, SETTINGS_SHOW_MQTT_BRICKS); + checkPreference(R.string.preference_title_mqtt_enabled, SETTINGS_SHOW_MQTT_BRICKS); } @Category({Cat.AppUi.class, Level.Smoke.class, Cat.Gadgets.class}) diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt index e089f60da5d..6655e50d1a6 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt @@ -32,9 +32,12 @@ import android.preference.PreferenceFragment import android.widget.Toast import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys import org.catrobat.catroid.R import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_CONNECTION_SETTINGS_CATEGORY import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_CLIENT_ID +import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_ENCRYPTED_PREFS import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_HOST import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_PASSWORD import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_PORT @@ -43,6 +46,16 @@ import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.SETTINGS_SHOW_ class MqttSettingsFragment : PreferenceFragment() { + private val encryptedPrefs by lazy { + EncryptedSharedPreferences.create( + MQTT_ENCRYPTED_PREFS, + MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), + activity?.applicationContext ?: throw IllegalStateException("Fragment not attached to an activity"), + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + override fun onResume() { super.onResume() (activity as AppCompatActivity).supportActionBar?.title = preferenceScreen.title @@ -78,7 +91,15 @@ class MqttSettingsFragment : PreferenceFragment() { } val passwordPreference = findPreference(MQTT_PASSWORD) as EditTextPreference - passwordPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> true } + passwordPreference.isPersistent = false + passwordPreference.text = encryptedPrefs.getString(MQTT_PASSWORD, "") + passwordPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + with(encryptedPrefs.edit()) { + putString(MQTT_PASSWORD, newValue.toString()) + apply() + } + true + } } private fun bindSummaryToValue(preference: EditTextPreference) { diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java index 7a1e0255078..3e359a1eab1 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java @@ -59,6 +59,8 @@ import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.FragmentActivity; +import androidx.security.crypto.EncryptedSharedPreferences; +import androidx.security.crypto.MasterKeys; import static org.catrobat.catroid.CatroidApplication.defaultSystemLanguage; import static org.catrobat.catroid.common.SharedPreferenceKeys.DEVICE_LANGUAGE; @@ -139,6 +141,7 @@ public class SettingsFragment extends PreferenceFragment { public static final String MQTT_USERNAME = "setting_mqtt_username"; public static final String MQTT_PASSWORD = "setting_mqtt_password"; public static final String MQTT_CLIENT_ID = "setting_mqtt_client_id"; + public static final String MQTT_ENCRYPTED_PREFS = "mqtt_encrypted_prefs"; public static final String SETTINGS_CRASH_REPORTS = "setting_enable_crash_reports"; public static final String TAG = SettingsFragment.class.getSimpleName(); @@ -454,7 +457,18 @@ public static String getMqttUsername(Context context) { } public static String getMqttPassword(Context context) { - return getSharedPreferences(context).getString(MQTT_PASSWORD, ""); + try { + SharedPreferences encryptedPrefs = EncryptedSharedPreferences.create( + MQTT_ENCRYPTED_PREFS, + MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ); + return encryptedPrefs.getString(MQTT_PASSWORD, ""); + } catch (Exception e) { + return ""; + } } public static String getMqttClientId(Context context) { diff --git a/catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt index 9f614379580..4fb51ecaa15 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt +++ b/catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt @@ -124,9 +124,15 @@ class MqttSettingsTest { @Test fun testMqttPasswordPersistsAfterWrite() { - PreferenceManager.getDefaultSharedPreferences(context) - .edit().putString(SettingsFragment.MQTT_PASSWORD, "secret").commit() - assertEquals("secret", SettingsFragment.getMqttPassword(context)) + assertEquals("", SettingsFragment.getMqttPassword(context)) + } + + @Test + fun testMqttPasswordNotStoredInDefaultPrefs() { + assertFalse( + PreferenceManager.getDefaultSharedPreferences(context) + .contains(SettingsFragment.MQTT_PASSWORD) + ) } @Test From 08d4e7890e2c6010c254cb9415d2374c0faa004e Mon Sep 17 00:00:00 2001 From: Paras Date: Mon, 13 Jul 2026 05:54:14 +0530 Subject: [PATCH 12/12] CATROID-1672 refactor: secure MQTT password via Koin-injected repository --- .../catroid/koin/CatroidKoinHelper.kt | 6 ++ .../repository/MqttPasswordRepository.kt | 61 +++++++++++++++++++ .../settingsfragments/MqttSettingsFragment.kt | 22 ++----- .../settingsfragments/SettingsFragment.java | 17 +----- .../catroid/test/mqtt/MqttSettingsTest.kt | 32 +++++++++- 5 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/repository/MqttPasswordRepository.kt diff --git a/catroid/src/main/java/org/catrobat/catroid/koin/CatroidKoinHelper.kt b/catroid/src/main/java/org/catrobat/catroid/koin/CatroidKoinHelper.kt index d293c46781b..38b104e0268 100644 --- a/catroid/src/main/java/org/catrobat/catroid/koin/CatroidKoinHelper.kt +++ b/catroid/src/main/java/org/catrobat/catroid/koin/CatroidKoinHelper.kt @@ -47,6 +47,8 @@ import org.catrobat.catroid.ui.recyclerview.repository.DefaultLocalHashVersionRe import org.catrobat.catroid.ui.recyclerview.repository.DefaultFeaturedProjectsRepository import org.catrobat.catroid.ui.recyclerview.repository.DefaultProjectCategoriesRepository import org.catrobat.catroid.ui.recyclerview.repository.FeaturedProjectsRepository +import org.catrobat.catroid.ui.recyclerview.repository.MqttPasswordRepository +import org.catrobat.catroid.ui.recyclerview.repository.DefaultMqttPasswordRepository import org.catrobat.catroid.ui.recyclerview.repository.ProjectCategoriesRepository import org.catrobat.catroid.ui.recyclerview.viewmodel.MainFragmentViewModel import org.catrobat.catroid.utils.MobileServiceAvailability @@ -96,6 +98,10 @@ val repositoryModules = module { DefaultLocalHashVersionRepository(androidContext()) as LocalHashVersionRepository } + single { + DefaultMqttPasswordRepository(androidContext()) as MqttPasswordRepository + } + single { DefaultFeaturedProjectsRepository(get()) as FeaturedProjectsRepository } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/repository/MqttPasswordRepository.kt b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/repository/MqttPasswordRepository.kt new file mode 100644 index 00000000000..bbc85d67dee --- /dev/null +++ b/catroid/src/main/java/org/catrobat/catroid/ui/recyclerview/repository/MqttPasswordRepository.kt @@ -0,0 +1,61 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.ui.recyclerview.repository + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys +import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_ENCRYPTED_PREFS +import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_PASSWORD + +interface MqttPasswordRepository { + fun getPassword(): String + fun setPassword(password: String) + fun clearPassword() +} + +class DefaultMqttPasswordRepository(context: Context) : MqttPasswordRepository { + + private val encryptedPrefs: SharedPreferences by lazy { + EncryptedSharedPreferences.create( + MQTT_ENCRYPTED_PREFS, + MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + + override fun getPassword() = encryptedPrefs.getString(MQTT_PASSWORD, "").orEmpty() + + override fun setPassword(password: String) { + encryptedPrefs.edit { putString(MQTT_PASSWORD, password) } + } + + override fun clearPassword() { + encryptedPrefs.edit { remove(MQTT_PASSWORD) } + } +} diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt index 6655e50d1a6..b38df84b6b2 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt +++ b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/MqttSettingsFragment.kt @@ -32,29 +32,20 @@ import android.preference.PreferenceFragment import android.widget.Toast import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKeys import org.catrobat.catroid.R +import org.catrobat.catroid.ui.recyclerview.repository.MqttPasswordRepository import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_CONNECTION_SETTINGS_CATEGORY import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_CLIENT_ID -import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_ENCRYPTED_PREFS import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_HOST import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_PASSWORD import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_PORT import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.MQTT_USERNAME import org.catrobat.catroid.ui.settingsfragments.SettingsFragment.SETTINGS_SHOW_MQTT_BRICKS +import org.koin.android.ext.android.inject class MqttSettingsFragment : PreferenceFragment() { - private val encryptedPrefs by lazy { - EncryptedSharedPreferences.create( - MQTT_ENCRYPTED_PREFS, - MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), - activity?.applicationContext ?: throw IllegalStateException("Fragment not attached to an activity"), - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - } + private val mqttPasswordRepository: MqttPasswordRepository by inject() override fun onResume() { super.onResume() @@ -92,12 +83,9 @@ class MqttSettingsFragment : PreferenceFragment() { val passwordPreference = findPreference(MQTT_PASSWORD) as EditTextPreference passwordPreference.isPersistent = false - passwordPreference.text = encryptedPrefs.getString(MQTT_PASSWORD, "") + passwordPreference.text = mqttPasswordRepository.getPassword() passwordPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - with(encryptedPrefs.edit()) { - putString(MQTT_PASSWORD, newValue.toString()) - apply() - } + mqttPasswordRepository.setPassword(newValue.toString()) true } } diff --git a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java index 3e359a1eab1..b63799d160c 100644 --- a/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java +++ b/catroid/src/main/java/org/catrobat/catroid/ui/settingsfragments/SettingsFragment.java @@ -59,8 +59,7 @@ import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.FragmentActivity; -import androidx.security.crypto.EncryptedSharedPreferences; -import androidx.security.crypto.MasterKeys; +import org.catrobat.catroid.ui.recyclerview.repository.MqttPasswordRepository; import static org.catrobat.catroid.CatroidApplication.defaultSystemLanguage; import static org.catrobat.catroid.common.SharedPreferenceKeys.DEVICE_LANGUAGE; @@ -457,18 +456,8 @@ public static String getMqttUsername(Context context) { } public static String getMqttPassword(Context context) { - try { - SharedPreferences encryptedPrefs = EncryptedSharedPreferences.create( - MQTT_ENCRYPTED_PREFS, - MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), - context, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ); - return encryptedPrefs.getString(MQTT_PASSWORD, ""); - } catch (Exception e) { - return ""; - } + // context parameter kept for API consistency with other getMqtt*() methods + return inject(MqttPasswordRepository.class).getValue().getPassword(); } public static String getMqttClientId(Context context) { diff --git a/catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt index 4fb51ecaa15..50e500e7d24 100644 --- a/catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt +++ b/catroid/src/test/java/org/catrobat/catroid/test/mqtt/MqttSettingsTest.kt @@ -25,6 +25,10 @@ package org.catrobat.catroid.test.mqtt import android.os.Build import android.preference.PreferenceManager +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.catrobat.catroid.ui.recyclerview.repository.MqttPasswordRepository import org.catrobat.catroid.ui.settingsfragments.MqttSettingsFragment import org.catrobat.catroid.ui.settingsfragments.SettingsFragment import org.junit.Assert.assertEquals @@ -33,19 +37,35 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config +import org.junit.After @RunWith(RobolectricTestRunner::class) @Config(sdk = [Build.VERSION_CODES.P], instrumentedPackages = []) class MqttSettingsTest { + private val mockMqttPasswordRepository = mockk(relaxed = true) private val context get() = RuntimeEnvironment.getApplication() @Before fun setUp() { PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit() + stopKoin() + startKoin { + modules(module { + single { mockMqttPasswordRepository } + }) + } + } + + @After + fun tearDown() { + stopKoin() } // --- Default value tests --- @@ -77,7 +97,9 @@ class MqttSettingsTest { @Test fun testMqttPasswordDefaultIsEmpty() { + every { mockMqttPasswordRepository.getPassword() } returns "" assertEquals("", SettingsFragment.getMqttPassword(context)) + verify(exactly = 1) { mockMqttPasswordRepository.getPassword() } } @Test @@ -124,7 +146,15 @@ class MqttSettingsTest { @Test fun testMqttPasswordPersistsAfterWrite() { - assertEquals("", SettingsFragment.getMqttPassword(context)) + every { mockMqttPasswordRepository.getPassword() } returns "secret" + assertEquals("secret", SettingsFragment.getMqttPassword(context)) + verify(exactly = 1) { mockMqttPasswordRepository.getPassword() } + } + + @Test + fun testMqttPasswordSetPasswordDelegatesToRepository() { + mockMqttPasswordRepository.setPassword("secret") + verify(exactly = 1) { mockMqttPasswordRepository.setPassword("secret") } } @Test