package net.igenius.mqttservice; import android.content.Intent; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import static net.igenius.mqttservice.MQTTServiceCommand.ACTION_CHECK_CONNECTION; import static net.igenius.mqttservice.MQTTServiceCommand.ACTION_CONNECT; import static net.igenius.mqttservice.MQTTServiceCommand.ACTION_CONNECT_AND_SUBSCRIBE; import static net.igenius.mqttservice.MQTTServiceCommand.ACTION_DISCONNECT; import static net.igenius.mqttservice.MQTTServiceCommand.ACTION_PUBLISH; import static net.igenius.mqttservice.MQTTServiceCommand.ACTION_SUBSCRIBE; import static net.igenius.mqttservice.MQTTServiceCommand.BROADCAST_CONNECTION_STATUS; import static net.igenius.mqttservice.MQTTServiceCommand.BROADCAST_CONNECTION_SUCCESS; import static net.igenius.mqttservice.MQTTServiceCommand.BROADCAST_EXCEPTION; import static net.igenius.mqttservice.MQTTServiceCommand.BROADCAST_MESSAGE_ARRIVED; import static net.igenius.mqttservice.MQTTServiceCommand.BROADCAST_PUBLISH_SUCCESS; import static net.igenius.mqttservice.MQTTServiceCommand.BROADCAST_SUBSCRIPTION_ERROR; import static net.igenius.mqttservice.MQTTServiceCommand.BROADCAST_SUBSCRIPTION_SUCCESS; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_AUTO_RESUBSCRIBE_ON_RECONNECT; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_BROADCAST_TYPE; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_BROKER_URL; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_CLIENT_ID; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_CONNECTED; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_EXCEPTION; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_PASSWORD; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_PAYLOAD; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_QOS; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_REQUEST_ID; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_TOPIC; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_TOPICS; import static net.igenius.mqttservice.MQTTServiceCommand.PARAM_USERNAME; import static net.igenius.mqttservice.MQTTServiceCommand.getBroadcastAction; public class MQTTService extends BackgroundService implements Runnable, MqttCallbackExtended { public static String NAMESPACE = "net.igenius.mqtt"; public static int KEEP_ALIVE_INTERVAL = 60; //measured in seconds public static int CONNECT_TIMEOUT = 30; //measured in seconds private BlockingQueue<Intent> mIntents = new LinkedBlockingQueue<>(); private MqttClient mClient; private boolean mShutdown = false; private String mConnectionRequestId = null; private HashMap<String, Integer> mTopicsToAutoResubscribe = new LinkedHashMap<>(); private String getParameter(Intent intent, String key) { return intent.getStringExtra(key); } private void broadcast(String type, String requestId, String... params) { if (params != null && params.length > 0 && params.length % 2 != 0) throw new IllegalArgumentException("Parameters must be passed in the form: PARAM_NAME, paramValue"); Intent intent = new Intent(); intent.setAction(getBroadcastAction()); intent.putExtra(PARAM_BROADCAST_TYPE, type); intent.putExtra(PARAM_REQUEST_ID, requestId); if (params != null && params.length > 0) { for (int i = 0; i <= params.length - 2; i += 2) { intent.putExtra(params[i], params[i + 1]); } } sendBroadcast(intent); } private void broadcastPayload(String type, String requestId, byte[] payload, String topic) { Intent intent = new Intent(); intent.setAction(getBroadcastAction()); intent.putExtra(PARAM_BROADCAST_TYPE, type); intent.putExtra(PARAM_REQUEST_ID, requestId); intent.putExtra(PARAM_PAYLOAD, payload); intent.putExtra(PARAM_TOPIC, topic); sendBroadcast(intent); } private void broadcastConnectionStatus(String requestId) { Intent intent = new Intent(); intent.setAction(getBroadcastAction()); intent.putExtra(PARAM_BROADCAST_TYPE, BROADCAST_CONNECTION_STATUS); intent.putExtra(PARAM_REQUEST_ID, requestId); intent.putExtra(PARAM_CONNECTED, mClient != null && mClient.isConnected()); sendBroadcast(intent); } private void broadcastException(String type, String requestId, Exception exception, String... params) { Intent intent = new Intent(); intent.setAction(getBroadcastAction()); intent.putExtra(PARAM_BROADCAST_TYPE, type); intent.putExtra(PARAM_REQUEST_ID, requestId); intent.putExtra(PARAM_EXCEPTION, exception); if (params != null && params.length > 0) { for (int i = 0; i <= params.length - 2; i += 2) { intent.putExtra(params[i], params[i + 1]); } } sendBroadcast(intent); } @Override public int onStartCommand(final Intent intent, int flags, int startId) { if (intent != null) { if (intent.getAction() == null || intent.getAction().isEmpty()) { MQTTServiceLogger.error("MQTTService onStartCommand", "null or empty Intent passed, ignoring it!"); } else { mShutdown = false; mIntents.offer(intent); post(this); } } if (mShutdown) { MQTTServiceLogger.debug(getClass().getSimpleName(), "Shutting down service"); stopSelf(); return START_NOT_STICKY; } return START_STICKY; } @Override public void run() { try { Intent intent = mIntents.take(); String action = intent.getAction(); String requestId = getParameter(intent, PARAM_REQUEST_ID); if (ACTION_CONNECT.equals(action) || ACTION_CONNECT_AND_SUBSCRIBE.equals(action)) { boolean connected = onConnect(requestId, getParameter(intent, PARAM_BROKER_URL), getParameter(intent, PARAM_CLIENT_ID), getParameter(intent, PARAM_USERNAME), getParameter(intent, PARAM_PASSWORD)); if (ACTION_CONNECT_AND_SUBSCRIBE.equals(action) && connected) { int qos = getInt(getParameter(intent, PARAM_QOS)); String[] topics = intent.getStringArrayExtra(PARAM_TOPICS); boolean autoResubscribe = intent.getBooleanExtra(PARAM_AUTO_RESUBSCRIBE_ON_RECONNECT, false); onSubscribe(requestId, qos, autoResubscribe, topics); } } else if (ACTION_DISCONNECT.equals(action)) { onDisconnect(requestId); } else if (ACTION_SUBSCRIBE.equals(action)) { onSubscribe(requestId, getInt(getParameter(intent, PARAM_QOS)), intent.getBooleanExtra(PARAM_AUTO_RESUBSCRIBE_ON_RECONNECT, false), intent.getStringArrayExtra(PARAM_TOPICS)); } else if (ACTION_PUBLISH.equals(action)) { onPublish(requestId, getParameter(intent, PARAM_TOPIC), intent.getByteArrayExtra(PARAM_PAYLOAD)); } else if (ACTION_CHECK_CONNECTION.equals(action)) { broadcastConnectionStatus(requestId); } } catch (Throwable exc) { MQTTServiceLogger.error(getClass().getSimpleName(), "Error while processing command", exc); } } private int getInt(String string) { try { return Integer.parseInt(string, 10); } catch (Throwable exc) { MQTTServiceLogger.error(getClass().getSimpleName(), "Unparsable string: " + string + ", returning 0"); return 0; } } private boolean onConnect(final String requestId, final String brokerUrl, final String clientId, final String username, final String password) { MQTTServiceLogger.debug(getClass().getSimpleName(), requestId + " Connect to " + brokerUrl + " with user: " + username + " and password: " + password); mConnectionRequestId = requestId; try { if (mClient == null) { MQTTServiceLogger.debug("onConnect", "Creating new MQTT connection"); mTopicsToAutoResubscribe.clear(); mClient = new MqttClient(brokerUrl, clientId, new MemoryPersistence()); mClient.setCallback(this); MqttConnectOptions connectOptions = new MqttConnectOptions(); if (username != null && password != null) { connectOptions.setUserName(username); connectOptions.setPassword(password.toCharArray()); } connectOptions.setCleanSession(true); connectOptions.setAutomaticReconnect(true); connectOptions.setKeepAliveInterval(KEEP_ALIVE_INTERVAL); connectOptions.setConnectionTimeout(CONNECT_TIMEOUT); mClient.connect(connectOptions); MQTTServiceLogger.debug("onConnect", "Connected"); } else { reconnect(requestId); } return true; } catch (Exception exc) { broadcastException(BROADCAST_EXCEPTION, requestId, new MqttException(exc)); return false; } } private void reconnect(String requestId) throws MqttException { if (mClient == null) return; if (mClient.isConnected()) { MQTTServiceLogger.debug("reconnect", "Client already connected, nothing to do"); } else { MQTTServiceLogger.debug("reconnect", "Reconnecting MQTT"); mClient.reconnect(); } } private boolean clientIsConnected() { return (mClient != null && mClient.isConnected()); } private void onDisconnect(final String requestId) { if (!clientIsConnected()) { MQTTServiceLogger.info("onDisconnect", "No client connected, nothing to disconnect!"); return; } try { MQTTServiceLogger.debug("onDisconnect", "Disconnecting MQTT"); mClient.disconnect(); } catch (Exception e) { MQTTServiceLogger.error("onDisconnect", "Error while disconnecting from MQTT. Request Id: " + requestId, e); try { mClient.disconnectForcibly(); } catch (Exception exc) { MQTTServiceLogger.error("onDisconnect", "Error while disconnect forcibly", exc); } } finally { mClient = null; mTopicsToAutoResubscribe.clear(); mShutdown = true; } } private void onSubscribe(final String requestId, final int qos, final boolean autoResubscribeOnConnect, final String... topics) { if (topics == null || topics.length == 0) { broadcastException(BROADCAST_SUBSCRIPTION_ERROR, requestId, new Exception("No topics passed to subscribe!"), PARAM_TOPIC, "" ); return; } if (!clientIsConnected()) { for (String topic : topics) { broadcastException(BROADCAST_SUBSCRIPTION_ERROR, requestId, new Exception("Can't subscribe to topics, client not connected!"), PARAM_TOPIC, topic ); } return; } for (String topic : topics) { try { MQTTServiceLogger.debug("onSubscribe", "Subscribing to topic: " + topic + " with QoS " + qos); mClient.subscribe(topic, qos); if (autoResubscribeOnConnect) { mTopicsToAutoResubscribe.put(topic, qos); } MQTTServiceLogger.debug("onSubscribe", "Successfully subscribed to topic: " + topic); broadcast(BROADCAST_SUBSCRIPTION_SUCCESS, requestId, PARAM_TOPIC, topic ); } catch (Exception e) { broadcastException(BROADCAST_SUBSCRIPTION_ERROR, requestId, new MqttException(e), PARAM_TOPIC, topic ); } } } private void onPublish(final String requestId, final String topic, final byte[] payload) { if (!clientIsConnected()) { broadcastException(BROADCAST_EXCEPTION, requestId, new Exception("Can't publish to topic: " + topic + ", client not connected!")); return; } try { MQTTServiceLogger.debug("onPublish", "Publishing to topic: " + topic + ", payload with size " + payload.length); MqttMessage message = new MqttMessage(payload); message.setQos(0); mClient.publish(topic, message); MQTTServiceLogger.debug("onPublish", "Successfully published to topic: " + topic + ", payload: " + payload); broadcast(BROADCAST_PUBLISH_SUCCESS, requestId, PARAM_TOPIC, topic ); } catch (Exception exc) { broadcastException(BROADCAST_EXCEPTION, requestId, new MqttException(exc)); } } @Override public void connectionLost(Throwable cause) { broadcastConnectionStatus(UUID.randomUUID().toString()); broadcastException(BROADCAST_EXCEPTION, UUID.randomUUID().toString(), new Exception(cause)); } @Override public void messageArrived(String topic, MqttMessage message) throws Exception { broadcastPayload(BROADCAST_MESSAGE_ARRIVED, UUID.randomUUID().toString(), message.getPayload(), topic); } @Override public void deliveryComplete(IMqttDeliveryToken token) { //TODO: check what this does } @Override public void connectComplete(boolean reconnect, String serverURI) { String requestId = reconnect ? UUID.randomUUID().toString() : mConnectionRequestId; if (reconnect) { MQTTServiceLogger.debug("reconnect", "Reconnected to " + serverURI); if (!mTopicsToAutoResubscribe.isEmpty()) { MQTTServiceLogger.debug("reconnect", "auto resubscribing to topics"); for (Map.Entry<String, Integer> entry : mTopicsToAutoResubscribe.entrySet()) { onSubscribe(requestId, entry.getValue(), true, entry.getKey()); } } } broadcastConnectionStatus(requestId); broadcast(BROADCAST_CONNECTION_SUCCESS, requestId); } @Override public void onDestroy() { // Disconnect the connection when the service gets destroyed onDisconnect("MQTTService@onDestroy"); super.onDestroy(); } }