Initial public release.
This commit is contained in:
parent
ed31acd60f
commit
58d87b11b7
249 changed files with 15831 additions and 4 deletions
58
mqtt-broker/build.gradle.kts
Normal file
58
mqtt-broker/build.gradle.kts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "club.clubk.ktag.apps.mqttbroker"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += listOf(
|
||||
"META-INF/INDEX.LIST",
|
||||
"META-INF/io.netty.versions.properties",
|
||||
"META-INF/DEPENDENCIES"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
implementation(project(":shared-services"))
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation("androidx.preference:preference:1.2.1")
|
||||
|
||||
// Moquette MQTT Broker
|
||||
implementation("io.moquette:moquette-broker:0.17")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
25
mqtt-broker/src/main/AndroidManifest.xml
Normal file
25
mqtt-broker/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name=".MqttBrokerService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<activity
|
||||
android:name=".BrokerSettingsActivity"
|
||||
android:exported="false"
|
||||
android:parentActivityName="club.clubk.ktag.apps.MainActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package club.clubk.ktag.apps.mqttbroker
|
||||
|
||||
object BrokerPreferenceKeys {
|
||||
const val BROKER_ENABLED = "broker_enabled"
|
||||
const val SSL_ENABLED = "broker_ssl_enabled"
|
||||
const val AUTH_ENABLED = "broker_auth_enabled"
|
||||
const val NSD_ENABLED = "broker_nsd_enabled"
|
||||
const val PORT = "broker_port"
|
||||
const val SSL_PORT = "broker_ssl_port"
|
||||
|
||||
const val DEFAULT_PORT = "1883"
|
||||
const val DEFAULT_SSL_PORT = "8883"
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package club.clubk.ktag.apps.mqttbroker
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import club.clubk.ktag.apps.sharedservices.SharedMqttClient
|
||||
|
||||
class BrokerSettingsActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(club.clubk.ktag.apps.sharedservices.R.layout.activity_settings)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(club.clubk.ktag.apps.sharedservices.R.id.settings_container, BrokerSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
if (MqttBrokerManager.isBrokerEnabled(this)) {
|
||||
MqttBrokerManager.startBrokerIfEnabled(this)
|
||||
} else {
|
||||
MqttBrokerManager.stopBroker(this)
|
||||
}
|
||||
SharedMqttClient.connect(this)
|
||||
super.finish()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createIntent(context: Context): Intent {
|
||||
return Intent(context, BrokerSettingsActivity::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package club.clubk.ktag.apps.mqttbroker
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import club.clubk.ktag.apps.sharedservices.MqttPreferenceKeys
|
||||
|
||||
class BrokerSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.broker_settings_pref, rootKey)
|
||||
|
||||
findPreference<Preference>("broker_manage_users")?.setOnPreferenceClickListener {
|
||||
showManageUsersDialog()
|
||||
true
|
||||
}
|
||||
|
||||
// Set up autodiscovery toggle to enable/disable manual connection fields
|
||||
val autodiscoveryPref = findPreference<SwitchPreferenceCompat>(MqttPreferenceKeys.AUTODISCOVERY)
|
||||
autodiscoveryPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
updateManualFieldsEnabled(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
updateManualFieldsEnabled(autodiscoveryPref?.isChecked ?: false)
|
||||
|
||||
updateManageUsersSummary()
|
||||
}
|
||||
|
||||
private fun updateManualFieldsEnabled(autodiscoveryEnabled: Boolean) {
|
||||
val disabled = autodiscoveryEnabled
|
||||
findPreference<Preference>(MqttPreferenceKeys.SERVER_URI)?.isEnabled = !disabled
|
||||
findPreference<Preference>(MqttPreferenceKeys.USERNAME)?.isEnabled = !disabled
|
||||
findPreference<Preference>(MqttPreferenceKeys.PASSWORD)?.isEnabled = !disabled
|
||||
}
|
||||
|
||||
private fun updateManageUsersSummary() {
|
||||
val count = CredentialManager.getCredentialCount(requireContext())
|
||||
findPreference<Preference>("broker_manage_users")?.summary =
|
||||
if (count == 0) "No users configured"
|
||||
else "$count user(s) configured"
|
||||
}
|
||||
|
||||
private fun showManageUsersDialog() {
|
||||
val context = requireContext()
|
||||
val credentials = CredentialManager.loadCredentials(context)
|
||||
val usernames = credentials.keys.toList()
|
||||
|
||||
val builder = AlertDialog.Builder(context)
|
||||
.setTitle("Manage Users")
|
||||
.setPositiveButton("Add User") { _, _ -> showAddUserDialog() }
|
||||
.setNegativeButton("Close", null)
|
||||
|
||||
if (usernames.isEmpty()) {
|
||||
builder.setMessage("No users configured. Add a user to enable authentication.")
|
||||
} else {
|
||||
builder.setItems(usernames.toTypedArray()) { _, which ->
|
||||
showRemoveUserDialog(usernames[which])
|
||||
}
|
||||
}
|
||||
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun showAddUserDialog() {
|
||||
val context = requireContext()
|
||||
|
||||
val layout = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
val pad = (16 * resources.displayMetrics.density).toInt()
|
||||
setPadding(pad, pad, pad, 0)
|
||||
}
|
||||
|
||||
val usernameInput = EditText(context).apply {
|
||||
hint = "Username"
|
||||
inputType = InputType.TYPE_CLASS_TEXT
|
||||
}
|
||||
layout.addView(usernameInput)
|
||||
|
||||
val passwordInput = EditText(context).apply {
|
||||
hint = "Password"
|
||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
}
|
||||
layout.addView(passwordInput)
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle("Add User")
|
||||
.setView(layout)
|
||||
.setPositiveButton("Add") { _, _ ->
|
||||
val username = usernameInput.text.toString().trim()
|
||||
val password = passwordInput.text.toString()
|
||||
|
||||
when {
|
||||
username.isEmpty() || password.isEmpty() -> {
|
||||
Toast.makeText(context, "Username and password are required", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
username.contains(":") -> {
|
||||
Toast.makeText(context, "Username cannot contain ':'", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
else -> {
|
||||
CredentialManager.addCredential(context, username, password)
|
||||
Toast.makeText(context, "User '$username' added", Toast.LENGTH_SHORT).show()
|
||||
updateManageUsersSummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showRemoveUserDialog(username: String) {
|
||||
val context = requireContext()
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle("Remove User")
|
||||
.setMessage("Remove user '$username'?")
|
||||
.setPositiveButton("Remove") { _, _ ->
|
||||
CredentialManager.removeCredential(context, username)
|
||||
Toast.makeText(context, "User '$username' removed", Toast.LENGTH_SHORT).show()
|
||||
updateManageUsersSummary()
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package club.clubk.ktag.apps.mqttbroker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class CredentialManager {
|
||||
private static final String TAG = "CredentialManager";
|
||||
private static final String CREDENTIALS_FILE = "mqtt_credentials.txt";
|
||||
|
||||
public static Map<String, String> loadCredentials(Context context) {
|
||||
Map<String, String> credentials = new LinkedHashMap<>();
|
||||
File file = new File(context.getFilesDir(), CREDENTIALS_FILE);
|
||||
if (!file.exists()) {
|
||||
return credentials;
|
||||
}
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) continue;
|
||||
int sep = line.indexOf(':');
|
||||
if (sep > 0 && sep < line.length() - 1) {
|
||||
String username = line.substring(0, sep);
|
||||
String hash = line.substring(sep + 1);
|
||||
credentials.put(username, hash);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error loading credentials", e);
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
public static void saveCredentials(Context context, Map<String, String> credentials) {
|
||||
File file = new File(context.getFilesDir(), CREDENTIALS_FILE);
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
|
||||
for (Map.Entry<String, String> entry : credentials.entrySet()) {
|
||||
writer.write(entry.getKey() + ":" + entry.getValue());
|
||||
writer.newLine();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error saving credentials", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void addCredential(Context context, String username, String password) {
|
||||
Map<String, String> credentials = loadCredentials(context);
|
||||
credentials.put(username, hashPassword(password));
|
||||
saveCredentials(context, credentials);
|
||||
}
|
||||
|
||||
public static void removeCredential(Context context, String username) {
|
||||
Map<String, String> credentials = loadCredentials(context);
|
||||
credentials.remove(username);
|
||||
saveCredentials(context, credentials);
|
||||
}
|
||||
|
||||
public static int getCredentialCount(Context context) {
|
||||
return loadCredentials(context).size();
|
||||
}
|
||||
|
||||
public static boolean checkCredential(String username, byte[] password, Map<String, String> credentials) {
|
||||
if (username == null || password == null || credentials == null) {
|
||||
return false;
|
||||
}
|
||||
String storedHash = credentials.get(username);
|
||||
if (storedHash == null) {
|
||||
return false;
|
||||
}
|
||||
String providedHash = hashPassword(new String(password, StandardCharsets.UTF_8));
|
||||
return storedHash.equals(providedHash);
|
||||
}
|
||||
|
||||
private static String hashPassword(String password) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) hexString.append('0');
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256 not available", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package club.clubk.ktag.apps.mqttbroker;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.moquette.broker.security.IAuthenticator;
|
||||
|
||||
public class FileAuthenticator implements IAuthenticator {
|
||||
private final Context context;
|
||||
|
||||
public FileAuthenticator(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkValid(String clientId, String username, byte[] password) {
|
||||
Map<String, String> credentials = CredentialManager.loadCredentials(context);
|
||||
return CredentialManager.checkCredential(username, password, credentials);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package club.clubk.ktag.apps.mqttbroker
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.text.format.Formatter
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
sealed class BrokerStatus {
|
||||
data object Stopped : BrokerStatus()
|
||||
data object Starting : BrokerStatus()
|
||||
data class Running(val port: Int, val sslPort: Int? = null) : BrokerStatus()
|
||||
data class Error(val message: String) : BrokerStatus()
|
||||
}
|
||||
|
||||
object MqttBrokerManager {
|
||||
|
||||
private val _status = MutableStateFlow<BrokerStatus>(BrokerStatus.Stopped)
|
||||
val status: StateFlow<BrokerStatus> = _status.asStateFlow()
|
||||
|
||||
@JvmStatic
|
||||
fun updateStatus(status: BrokerStatus) {
|
||||
_status.value = status
|
||||
}
|
||||
|
||||
fun isBrokerEnabled(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(BrokerPreferenceKeys.BROKER_ENABLED, false)
|
||||
}
|
||||
|
||||
fun startBroker(context: Context) {
|
||||
val intent = Intent(context, MqttBrokerService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopBroker(context: Context) {
|
||||
val intent = Intent(context, MqttBrokerService::class.java)
|
||||
context.stopService(intent)
|
||||
}
|
||||
|
||||
fun startBrokerIfEnabled(context: Context) {
|
||||
if (isBrokerEnabled(context)) {
|
||||
startBroker(context)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun getDeviceIpAddress(context: Context): String? {
|
||||
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
|
||||
?: return null
|
||||
val ipInt = wifiManager.connectionInfo.ipAddress
|
||||
if (ipInt == 0) return null
|
||||
return Formatter.formatIpAddress(ipInt)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,326 @@
|
|||
package club.clubk.ktag.apps.mqttbroker;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.nsd.NsdManager;
|
||||
import android.net.nsd.NsdServiceInfo;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.widget.Toast;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import club.clubk.ktag.apps.sharedservices.MqttPreferenceKeys;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import io.moquette.broker.Server;
|
||||
import io.moquette.broker.config.MemoryConfig;
|
||||
import io.moquette.interception.AbstractInterceptHandler;
|
||||
import io.moquette.interception.InterceptHandler;
|
||||
import io.moquette.interception.messages.InterceptConnectMessage;
|
||||
import io.moquette.interception.messages.InterceptDisconnectMessage;
|
||||
import io.moquette.interception.messages.InterceptPublishMessage;
|
||||
import io.moquette.interception.messages.InterceptSubscribeMessage;
|
||||
import io.moquette.interception.messages.InterceptUnsubscribeMessage;
|
||||
|
||||
public class MqttBrokerService extends Service {
|
||||
private static final String TAG = "MqttBrokerService";
|
||||
private static final String CHANNEL_ID = "MqttBrokerChannel";
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
|
||||
private Server mqttBroker;
|
||||
private int port = 1883;
|
||||
private int sslPort = 8883;
|
||||
private boolean sslEnabled = false;
|
||||
private boolean authEnabled = false;
|
||||
private boolean nsdEnabled = false;
|
||||
private NsdManager nsdManager;
|
||||
private NsdManager.RegistrationListener nsdRegistrationListener;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
createNotificationChannel();
|
||||
Log.d(TAG, "Service created");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
stopBroker();
|
||||
loadConfiguration();
|
||||
startForeground(NOTIFICATION_ID, createNotification("MQTT Broker is starting..."));
|
||||
startBroker();
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void loadConfiguration() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
sslEnabled = prefs.getBoolean(BrokerPreferenceKeys.SSL_ENABLED, false);
|
||||
authEnabled = prefs.getBoolean(BrokerPreferenceKeys.AUTH_ENABLED, false);
|
||||
nsdEnabled = prefs.getBoolean(BrokerPreferenceKeys.NSD_ENABLED, false);
|
||||
port = Integer.parseInt(prefs.getString(BrokerPreferenceKeys.PORT, "1883"));
|
||||
sslPort = Integer.parseInt(prefs.getString(BrokerPreferenceKeys.SSL_PORT, "8883"));
|
||||
}
|
||||
|
||||
public boolean isBrokerRunning() {
|
||||
return mqttBroker != null;
|
||||
}
|
||||
|
||||
private void startBroker() {
|
||||
MqttBrokerManager.updateStatus(BrokerStatus.Starting.INSTANCE);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
mqttBroker = new Server();
|
||||
|
||||
Properties configProps = new Properties();
|
||||
configProps.setProperty("host", "0.0.0.0");
|
||||
configProps.setProperty("port", String.valueOf(port));
|
||||
configProps.setProperty("allow_anonymous", String.valueOf(!authEnabled));
|
||||
configProps.setProperty("allow_zero_byte_client_id", "true");
|
||||
configProps.setProperty("reauthorize_subscriptions_on_connect", "false");
|
||||
|
||||
// SSL Configuration
|
||||
if (sslEnabled) {
|
||||
if (!SSLCertificateManager.keystoreExists(getApplicationContext())) {
|
||||
Log.d(TAG, "Keystore not found, generating self-signed certificate...");
|
||||
SSLCertificateManager.generateSelfSignedCertificate(getApplicationContext());
|
||||
}
|
||||
|
||||
configProps.setProperty("ssl_port", String.valueOf(sslPort));
|
||||
configProps.setProperty("jks_path",
|
||||
SSLCertificateManager.getKeystoreFile(getApplicationContext()).getAbsolutePath());
|
||||
configProps.setProperty("key_store_type", "pkcs12");
|
||||
configProps.setProperty("key_store_password", SSLCertificateManager.getKeystorePassword());
|
||||
configProps.setProperty("key_manager_password", SSLCertificateManager.getKeystorePassword());
|
||||
|
||||
Log.d(TAG, "SSL enabled on port " + sslPort);
|
||||
}
|
||||
|
||||
// Persistent data path
|
||||
File dataDir = new File(getFilesDir(), "moquette_data");
|
||||
if (!dataDir.exists()) {
|
||||
dataDir.mkdirs();
|
||||
}
|
||||
configProps.setProperty("persistent_store", dataDir.getAbsolutePath());
|
||||
|
||||
MemoryConfig memoryConfig = new MemoryConfig(configProps);
|
||||
|
||||
List<InterceptHandler> handlers = new ArrayList<>();
|
||||
handlers.add(new BrokerInterceptor());
|
||||
|
||||
// Ensure KTag autodiscovery credential exists
|
||||
ensureKTagCredential();
|
||||
|
||||
if (authEnabled) {
|
||||
FileAuthenticator authenticator = new FileAuthenticator(getApplicationContext());
|
||||
mqttBroker.startServer(memoryConfig, handlers, null, authenticator, null);
|
||||
} else {
|
||||
mqttBroker.startServer(memoryConfig, handlers);
|
||||
}
|
||||
|
||||
String statusMsg = "Broker running on port " + port;
|
||||
if (sslEnabled) {
|
||||
statusMsg += " (SSL: " + sslPort + ")";
|
||||
}
|
||||
if (authEnabled) {
|
||||
statusMsg += " (Auth)";
|
||||
}
|
||||
if (nsdEnabled) {
|
||||
statusMsg += " (mDNS)";
|
||||
}
|
||||
|
||||
Log.d(TAG, "MQTT Broker started - " + statusMsg);
|
||||
updateNotification(statusMsg);
|
||||
MqttBrokerManager.updateStatus(new BrokerStatus.Running(port,
|
||||
sslEnabled ? Integer.valueOf(sslPort) : null));
|
||||
|
||||
if (nsdEnabled) {
|
||||
registerNsdService();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to start broker", e);
|
||||
updateNotification("Failed to start: " + e.getMessage());
|
||||
MqttBrokerManager.updateStatus(new BrokerStatus.Error(
|
||||
e.getMessage() != null ? e.getMessage() : "Unknown error"));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void ensureKTagCredential() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
String battlefield = prefs.getString(
|
||||
MqttPreferenceKeys.BATTLEFIELD,
|
||||
MqttPreferenceKeys.DEFAULT_BATTLEFIELD);
|
||||
CredentialManager.addCredential(getApplicationContext(), "KTag", battlefield);
|
||||
Log.d(TAG, "Ensured KTag credential with battlefield: " + battlefield);
|
||||
}
|
||||
|
||||
private void stopBroker() {
|
||||
unregisterNsdService();
|
||||
if (mqttBroker != null) {
|
||||
mqttBroker.stopServer();
|
||||
mqttBroker = null;
|
||||
}
|
||||
MqttBrokerManager.updateStatus(BrokerStatus.Stopped.INSTANCE);
|
||||
}
|
||||
|
||||
private void registerNsdService() {
|
||||
NsdServiceInfo serviceInfo = new NsdServiceInfo();
|
||||
serviceInfo.setServiceName("MQTT Broker - KTag");
|
||||
serviceInfo.setServiceType("_mqtt._tcp.");
|
||||
serviceInfo.setPort(port);
|
||||
serviceInfo.setAttribute("purpose", "KTag MQTT Broker");
|
||||
serviceInfo.setAttribute("version", "1");
|
||||
serviceInfo.setAttribute("ssl", String.valueOf(sslEnabled));
|
||||
serviceInfo.setAttribute("auth", String.valueOf(authEnabled));
|
||||
|
||||
nsdManager = (NsdManager) getSystemService(Context.NSD_SERVICE);
|
||||
nsdRegistrationListener = new NsdManager.RegistrationListener() {
|
||||
@Override
|
||||
public void onServiceRegistered(NsdServiceInfo info) {
|
||||
Log.d(TAG, "NSD service registered: " + info.getServiceName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegistrationFailed(NsdServiceInfo info, int errorCode) {
|
||||
Log.e(TAG, "NSD registration failed: " + errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceUnregistered(NsdServiceInfo info) {
|
||||
Log.d(TAG, "NSD service unregistered");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnregistrationFailed(NsdServiceInfo info, int errorCode) {
|
||||
Log.e(TAG, "NSD unregistration failed: " + errorCode);
|
||||
}
|
||||
};
|
||||
|
||||
nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, nsdRegistrationListener);
|
||||
}
|
||||
|
||||
private void unregisterNsdService() {
|
||||
if (nsdManager != null && nsdRegistrationListener != null) {
|
||||
try {
|
||||
nsdManager.unregisterService(nsdRegistrationListener);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, "NSD service already unregistered", e);
|
||||
}
|
||||
nsdRegistrationListener = null;
|
||||
nsdManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
new Thread(this::stopBroker).start();
|
||||
Log.d(TAG, "Service destroyed");
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"MQTT Broker Service",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("MQTT Broker background service");
|
||||
|
||||
NotificationManager manager = getSystemService(NotificationManager.class);
|
||||
if (manager != null) {
|
||||
manager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Notification createNotification(String contentText) {
|
||||
Intent notificationIntent;
|
||||
try {
|
||||
notificationIntent = new Intent(this,
|
||||
Class.forName("club.clubk.ktag.apps.MainActivity"));
|
||||
} catch (ClassNotFoundException e) {
|
||||
notificationIntent = new Intent();
|
||||
}
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("MQTT Broker")
|
||||
.setContentText(contentText)
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void updateNotification(String contentText) {
|
||||
NotificationManager manager = getSystemService(NotificationManager.class);
|
||||
if (manager != null) {
|
||||
manager.notify(NOTIFICATION_ID, createNotification(contentText));
|
||||
}
|
||||
}
|
||||
|
||||
private class BrokerInterceptor extends AbstractInterceptHandler {
|
||||
@Override
|
||||
public String getID() {
|
||||
return "BrokerMonitor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnect(InterceptConnectMessage msg) {
|
||||
Log.d(TAG, "Client connected: " + msg.getClientID());
|
||||
new Handler(Looper.getMainLooper()).post(() ->
|
||||
Toast.makeText(MqttBrokerService.this,
|
||||
"MQTT: " + msg.getClientID() + " connected",
|
||||
Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(InterceptDisconnectMessage msg) {
|
||||
Log.d(TAG, "Client disconnected: " + msg.getClientID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPublish(InterceptPublishMessage msg) {
|
||||
Log.d(TAG, "Message published to " + msg.getTopicName() + " by " + msg.getClientID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(InterceptSubscribeMessage msg) {
|
||||
Log.d(TAG, msg.getClientID() + " subscribed to " + msg.getTopicFilter());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnsubscribe(InterceptUnsubscribeMessage msg) {
|
||||
Log.d(TAG, msg.getClientID() + " unsubscribed from " + msg.getTopicFilter());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionLoopError(Throwable ex) {
|
||||
Log.e(TAG, "Session loop error", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
package club.clubk.ktag.apps.mqttbroker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Signature;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
|
||||
public class SSLCertificateManager {
|
||||
private static final String TAG = "SSLCertManager";
|
||||
private static final String KEYSTORE_FILE = "mqtt_broker.p12";
|
||||
private static final String KEYSTORE_PASSWORD = "mqtt_broker_password";
|
||||
private static final String KEY_ALIAS = "mqtt_broker_key";
|
||||
|
||||
public static File getKeystoreFile(Context context) {
|
||||
return new File(context.getFilesDir(), KEYSTORE_FILE);
|
||||
}
|
||||
|
||||
public static String getKeystorePassword() {
|
||||
return KEYSTORE_PASSWORD;
|
||||
}
|
||||
|
||||
public static String getKeyAlias() {
|
||||
return KEY_ALIAS;
|
||||
}
|
||||
|
||||
public static boolean keystoreExists(Context context) {
|
||||
return getKeystoreFile(context).exists();
|
||||
}
|
||||
|
||||
public static void generateSelfSignedCertificate(Context context) throws Exception {
|
||||
Log.d(TAG, "Generating self-signed certificate...");
|
||||
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(2048, new SecureRandom());
|
||||
KeyPair keyPair = keyGen.generateKeyPair();
|
||||
PrivateKey privateKey = keyPair.getPrivate();
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
Date notBefore = new Date(now - 24 * 60 * 60 * 1000);
|
||||
Date notAfter = new Date(now + 365L * 24 * 60 * 60 * 1000);
|
||||
|
||||
BigInteger serial = BigInteger.valueOf(now);
|
||||
String dn = "CN=MQTT Broker, O=KTag Apps, C=US";
|
||||
byte[] certDer = buildSelfSignedCertificateDer(
|
||||
keyPair, privateKey, serial, notBefore, notAfter, dn);
|
||||
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
X509Certificate cert = (X509Certificate) cf.generateCertificate(
|
||||
new ByteArrayInputStream(certDer));
|
||||
|
||||
cert.verify(keyPair.getPublic());
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||
keyStore.load(null, null);
|
||||
|
||||
Certificate[] chain = new Certificate[]{cert};
|
||||
keyStore.setKeyEntry(KEY_ALIAS, privateKey, KEYSTORE_PASSWORD.toCharArray(), chain);
|
||||
|
||||
File keystoreFile = getKeystoreFile(context);
|
||||
try (FileOutputStream fos = new FileOutputStream(keystoreFile)) {
|
||||
keyStore.store(fos, KEYSTORE_PASSWORD.toCharArray());
|
||||
}
|
||||
|
||||
Log.d(TAG, "Self-signed certificate generated and saved to " + keystoreFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
public static KeyStore loadKeystore(Context context) throws Exception {
|
||||
File keystoreFile = getKeystoreFile(context);
|
||||
if (!keystoreFile.exists()) {
|
||||
throw new Exception("Keystore file does not exist");
|
||||
}
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||
try (FileInputStream fis = new FileInputStream(keystoreFile)) {
|
||||
keyStore.load(fis, KEYSTORE_PASSWORD.toCharArray());
|
||||
}
|
||||
|
||||
return keyStore;
|
||||
}
|
||||
|
||||
public static void deleteKeystore(Context context) {
|
||||
File keystoreFile = getKeystoreFile(context);
|
||||
if (keystoreFile.exists()) {
|
||||
if (keystoreFile.delete()) {
|
||||
Log.d(TAG, "Keystore deleted");
|
||||
} else {
|
||||
Log.e(TAG, "Failed to delete keystore");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getCertificateInfo(Context context) {
|
||||
try {
|
||||
KeyStore keyStore = loadKeystore(context);
|
||||
X509Certificate cert = (X509Certificate) keyStore.getCertificate(KEY_ALIAS);
|
||||
|
||||
if (cert != null) {
|
||||
StringBuilder info = new StringBuilder();
|
||||
info.append("Subject: ").append(cert.getSubjectDN().getName()).append("\n");
|
||||
info.append("Issuer: ").append(cert.getIssuerDN().getName()).append("\n");
|
||||
info.append("Valid from: ").append(cert.getNotBefore()).append("\n");
|
||||
info.append("Valid until: ").append(cert.getNotAfter()).append("\n");
|
||||
info.append("Serial: ").append(cert.getSerialNumber()).append("\n");
|
||||
return info.toString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error getting certificate info", e);
|
||||
}
|
||||
return "Certificate info not available";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// DER / ASN.1 certificate builder using only java.security APIs
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private static byte[] buildSelfSignedCertificateDer(
|
||||
KeyPair keyPair, PrivateKey privateKey,
|
||||
BigInteger serial, Date notBefore, Date notAfter,
|
||||
String distinguishedName) throws Exception {
|
||||
|
||||
byte[] tbsCert = buildTbsCertificate(
|
||||
serial, notBefore, notAfter, distinguishedName,
|
||||
keyPair.getPublic().getEncoded());
|
||||
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initSign(privateKey);
|
||||
sig.update(tbsCert);
|
||||
byte[] signature = sig.sign();
|
||||
|
||||
byte[] signatureAlgorithm = derSequence(new byte[][]{
|
||||
derOid(new int[]{1, 2, 840, 113549, 1, 1, 11}),
|
||||
derNull()
|
||||
});
|
||||
|
||||
byte[] signatureBits = new byte[signature.length + 1];
|
||||
signatureBits[0] = 0x00;
|
||||
System.arraycopy(signature, 0, signatureBits, 1, signature.length);
|
||||
byte[] signatureBitString = derBitString(signatureBits);
|
||||
|
||||
return derSequence(new byte[][]{tbsCert, signatureAlgorithm, signatureBitString});
|
||||
}
|
||||
|
||||
private static byte[] buildTbsCertificate(
|
||||
BigInteger serial, Date notBefore, Date notAfter,
|
||||
String distinguishedName, byte[] publicKeyEncoded) throws Exception {
|
||||
|
||||
byte[] version = derExplicitTag(0, derInteger(BigInteger.valueOf(2)));
|
||||
byte[] serialNumber = derInteger(serial);
|
||||
|
||||
byte[] signatureAlgorithm = derSequence(new byte[][]{
|
||||
derOid(new int[]{1, 2, 840, 113549, 1, 1, 11}),
|
||||
derNull()
|
||||
});
|
||||
|
||||
byte[] issuer = encodeDN(distinguishedName);
|
||||
byte[] subject = issuer;
|
||||
|
||||
byte[] validity = derSequence(new byte[][]{
|
||||
derUtcTime(notBefore),
|
||||
derUtcTime(notAfter)
|
||||
});
|
||||
|
||||
byte[] subjectPublicKeyInfo = publicKeyEncoded;
|
||||
|
||||
return derSequence(new byte[][]{
|
||||
version, serialNumber, signatureAlgorithm,
|
||||
issuer, validity, subject, subjectPublicKeyInfo
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] encodeDN(String dn) {
|
||||
String[] parts = dn.split(",");
|
||||
byte[][] rdnSets = new byte[parts.length][];
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
String part = parts[i].trim();
|
||||
int eq = part.indexOf('=');
|
||||
String key = part.substring(0, eq).trim();
|
||||
String value = part.substring(eq + 1).trim();
|
||||
rdnSets[i] = derSet(new byte[][]{
|
||||
derSequence(new byte[][]{
|
||||
derOidForAttributeType(key),
|
||||
derUtf8String(value)
|
||||
})
|
||||
});
|
||||
}
|
||||
return derSequence(rdnSets);
|
||||
}
|
||||
|
||||
private static byte[] derOidForAttributeType(String type) {
|
||||
switch (type.toUpperCase()) {
|
||||
case "CN": return derOid(new int[]{2, 5, 4, 3});
|
||||
case "O": return derOid(new int[]{2, 5, 4, 10});
|
||||
case "C": return derOid(new int[]{2, 5, 4, 6});
|
||||
case "OU": return derOid(new int[]{2, 5, 4, 11});
|
||||
case "ST": return derOid(new int[]{2, 5, 4, 8});
|
||||
case "L": return derOid(new int[]{2, 5, 4, 7});
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported attribute type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] derSequence(byte[][] items) {
|
||||
return derConstructed(0x30, items);
|
||||
}
|
||||
|
||||
private static byte[] derSet(byte[][] items) {
|
||||
return derConstructed(0x31, items);
|
||||
}
|
||||
|
||||
private static byte[] derConstructed(int tag, byte[][] items) {
|
||||
int totalLen = 0;
|
||||
for (byte[] item : items) totalLen += item.length;
|
||||
byte[] content = new byte[totalLen];
|
||||
int offset = 0;
|
||||
for (byte[] item : items) {
|
||||
System.arraycopy(item, 0, content, offset, item.length);
|
||||
offset += item.length;
|
||||
}
|
||||
return derWrap(tag, content);
|
||||
}
|
||||
|
||||
private static byte[] derExplicitTag(int tagNumber, byte[] content) {
|
||||
return derWrap(0xA0 | tagNumber, content);
|
||||
}
|
||||
|
||||
private static byte[] derInteger(BigInteger value) {
|
||||
byte[] encoded = value.toByteArray();
|
||||
return derWrap(0x02, encoded);
|
||||
}
|
||||
|
||||
private static byte[] derBitString(byte[] content) {
|
||||
return derWrap(0x03, content);
|
||||
}
|
||||
|
||||
private static byte[] derNull() {
|
||||
return new byte[]{0x05, 0x00};
|
||||
}
|
||||
|
||||
private static byte[] derUtf8String(String s) {
|
||||
byte[] content = s.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
return derWrap(0x0C, content);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static byte[] derUtcTime(Date date) {
|
||||
java.util.Calendar cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"));
|
||||
cal.setTime(date);
|
||||
String s = String.format(java.util.Locale.US, "%02d%02d%02d%02d%02d%02dZ",
|
||||
cal.get(java.util.Calendar.YEAR) % 100,
|
||||
cal.get(java.util.Calendar.MONTH) + 1,
|
||||
cal.get(java.util.Calendar.DAY_OF_MONTH),
|
||||
cal.get(java.util.Calendar.HOUR_OF_DAY),
|
||||
cal.get(java.util.Calendar.MINUTE),
|
||||
cal.get(java.util.Calendar.SECOND));
|
||||
return derWrap(0x17, s.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
private static byte[] derOid(int[] components) {
|
||||
java.io.ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream();
|
||||
buf.write(40 * components[0] + components[1]);
|
||||
for (int i = 2; i < components.length; i++) {
|
||||
encodeOidComponent(buf, components[i]);
|
||||
}
|
||||
return derWrap(0x06, buf.toByteArray());
|
||||
}
|
||||
|
||||
private static void encodeOidComponent(java.io.ByteArrayOutputStream buf, int value) {
|
||||
if (value < 128) {
|
||||
buf.write(value);
|
||||
} else {
|
||||
byte[] temp = new byte[5];
|
||||
int pos = temp.length;
|
||||
temp[--pos] = (byte) (value & 0x7F);
|
||||
value >>= 7;
|
||||
while (value > 0) {
|
||||
temp[--pos] = (byte) (0x80 | (value & 0x7F));
|
||||
value >>= 7;
|
||||
}
|
||||
buf.write(temp, pos, temp.length - pos);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] derWrap(int tag, byte[] content) {
|
||||
byte[] lenBytes = derLength(content.length);
|
||||
byte[] result = new byte[1 + lenBytes.length + content.length];
|
||||
result[0] = (byte) tag;
|
||||
System.arraycopy(lenBytes, 0, result, 1, lenBytes.length);
|
||||
System.arraycopy(content, 0, result, 1 + lenBytes.length, content.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] derLength(int length) {
|
||||
if (length < 128) {
|
||||
return new byte[]{(byte) length};
|
||||
} else if (length < 256) {
|
||||
return new byte[]{(byte) 0x81, (byte) length};
|
||||
} else if (length < 65536) {
|
||||
return new byte[]{(byte) 0x82, (byte) (length >> 8), (byte) length};
|
||||
} else {
|
||||
return new byte[]{(byte) 0x83, (byte) (length >> 16), (byte) (length >> 8), (byte) length};
|
||||
}
|
||||
}
|
||||
}
|
||||
111
mqtt-broker/src/main/res/xml/broker_settings_pref.xml
Normal file
111
mqtt-broker/src/main/res/xml/broker_settings_pref.xml
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory android:title="Client Connection">
|
||||
|
||||
<club.clubk.ktag.apps.sharedservices.SummarizedEditTextPreference
|
||||
app:customHint="A name for this device"
|
||||
android:inputType="text"
|
||||
android:key="mqtt_device_id"
|
||||
android:summary="%s"
|
||||
android:title="Device ID" />
|
||||
|
||||
<club.clubk.ktag.apps.sharedservices.SummarizedEditTextPreference
|
||||
app:customHint="Your assigned section of the MQTT server"
|
||||
android:inputType="text"
|
||||
android:key="mqtt_battlefield"
|
||||
android:summary="%s"
|
||||
android:title="KTag Battlefield" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="mqtt_autodiscovery"
|
||||
android:title="Autodiscovery"
|
||||
android:summary="Automatically find a broker on the local network"
|
||||
android:defaultValue="true" />
|
||||
|
||||
<club.clubk.ktag.apps.sharedservices.SummarizedEditTextPreference
|
||||
app:customHint="ssl://mqtt.clubk.club:8883"
|
||||
android:inputType="textUri"
|
||||
android:key="mqtt_server_uri"
|
||||
android:summary="%s"
|
||||
android:title="KTag Server" />
|
||||
|
||||
<club.clubk.ktag.apps.sharedservices.SummarizedEditTextPreference
|
||||
app:customHint="Enter your MQTT username"
|
||||
android:inputType="text"
|
||||
android:key="mqtt_username"
|
||||
android:summary="%s"
|
||||
android:title="Username" />
|
||||
|
||||
<club.clubk.ktag.apps.sharedservices.SummarizedEditTextPreference
|
||||
app:customHint="Enter your MQTT password"
|
||||
android:inputType="textPassword"
|
||||
android:key="mqtt_password"
|
||||
android:summary="shh...it's a secret!"
|
||||
android:title="Password" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="Broker">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="broker_enabled"
|
||||
android:title="Enable MQTT Broker"
|
||||
android:summary="Run an embedded MQTT broker on this device"
|
||||
android:defaultValue="false" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="Network">
|
||||
|
||||
<EditTextPreference
|
||||
android:key="broker_port"
|
||||
android:title="MQTT Port"
|
||||
android:summary="Default: 1883"
|
||||
android:defaultValue="1883"
|
||||
android:inputType="number" />
|
||||
|
||||
<EditTextPreference
|
||||
android:key="broker_ssl_port"
|
||||
android:title="SSL Port"
|
||||
android:summary="Default: 8883"
|
||||
android:defaultValue="8883"
|
||||
android:inputType="number"
|
||||
android:dependency="broker_ssl_enabled" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="Security">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="broker_ssl_enabled"
|
||||
android:title="Enable SSL/TLS"
|
||||
android:summary="Use a self-signed certificate for encrypted connections"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="broker_auth_enabled"
|
||||
android:title="Enable Authentication"
|
||||
android:summary="Require username/password to connect"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<Preference
|
||||
android:key="broker_manage_users"
|
||||
android:title="Manage Users"
|
||||
android:summary="Add or remove user credentials"
|
||||
android:dependency="broker_auth_enabled" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="Discovery">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="broker_nsd_enabled"
|
||||
android:title="Enable mDNS Discovery"
|
||||
android:summary="Advertise this broker on the local network"
|
||||
android:defaultValue="false" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
Loading…
Add table
Add a link
Reference in a new issue