com.amazonaws.mobileconnectors.iot.AWSIotMqttManager.java Source code

Java tutorial

Introduction

Here is the source code for com.amazonaws.mobileconnectors.iot.AWSIotMqttManager.java

Source

/**
 * Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *    http://aws.amazon.com/apache2.0
 *
 * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
 * OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and
 * limitations under the License.
 */

package com.amazonaws.mobileconnectors.iot;

import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;

import com.amazonaws.AmazonClientException;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.regions.Region;
import com.amazonaws.util.StringUtils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.eclipse.paho.client.mqttv3.IMqttActionListener;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.IMqttToken;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttCallback;
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.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.net.SocketFactory;

/**
 * The broker for applications allows receive and publish messages AWS IoT
 * MqttClient Amazon Internet of Things AWSIotMqttManager implements a broker
 * for applications and internet-connected things to publish and receive
 * messages.
 */
public class AWSIotMqttManager {

    /** API level 16. 16 = Build.VERSION_CODES.JELLY_BEAN. */
    private static final Integer ANDROID_API_LEVEL_16 = 16;
    /** Conversion seconds to milliseconds. */
    private static final Integer MILLIS_IN_ONE_SECOND = 1000;

    private static final Log LOGGER = LogFactory.getLog(AWSIotMqttManager.class);
    /** Constant for number of tokens in endpoint. */
    private static final int ENDPOINT_SPLIT_SIZE = 5;
    /** Constant for token offset of "iot" in endpoint. */
    private static final int ENDPOINT_IOT_OFFSET = 1;
    /** Constant for token offset of "amazonaws" in endpoint. */
    private static final int ENDPOINT_DOMAIN_OFFSET = 3;
    /** Constant for token offset of "com" in endpoint. */
    private static final int ENDPOINT_TLD_OFFSET = 4;

    /** Default value for starting delay in exponential backoff reconnect algorithm. */
    public static final Integer DEFAULT_MIN_RECONNECT_RETRY_TIME_SECONDS = 4;
    /** Default value for maximum delay in exponential backoff reconnect algorithm. */
    public static final Integer DEFAULT_MAX_RECONNECT_RETRY_TIME_SECONDS = 64;
    /** Default value for reconnect enabled. */
    public static final Boolean DEFAULT_AUTO_RECONNECT_ENABLED = true;
    /** Default value for number of auto reconnect attempts before giving up. */
    public static final Integer DEFAULT_AUTO_RECONNECT_ATTEMPTS = 10;
    /** Default value for MQTT keep alive. */
    public static final Integer DEFAULT_KEEP_ALIVE_SECONDS = 10;
    /** Default value for offline publish queue enabled. */
    public static final Boolean DEFAULT_OFFLINE_PUBLISH_QUEUE_ENABLED = true;
    /** Default value for offline publish queue bound. */
    public static final Integer DEFAULT_OFFLINE_PUBLISH_QUEUE_BOUND = 100;
    /** Constant for milliseconds between queue publishes. */
    private static final Long DEFAULT_MILLIS_BETWEEN_QUEUE_PUBLISHES = 250L;
    /** Default value for "connection established" hysteresis timer. */
    private static final Integer DEFAULT_CONNECTION_STABILITY_TIME_SECONDS = 10;

    /** The underlying Paho Java MQTT client. */
    private MqttAsyncClient mqttClient;

    /** MQTT broker URL.  Built from region, customer endpoint. */
    private String mqttBrokerURL;

    /** WebSocket URL Signer object. */
    private AWSIotWebSocketUrlSigner signer;

    /** Customer specific prefix for data endpoint. */
    private final String accountEndpointPrefix;

    /** MQTT client ID, used for both initial connection and reconnections. */
    private final String mqttClientId;
    /** AWS IoT region hosting the MQTT service. */
    private final Region region;

    /** Is this client a WebSocket Client?  Setup on initial connect and then used for reconnect logic. */
    private Boolean isWebSocketClient;

    /**
     * Connection callback for the user.  This client receives the connection
     * lost callback and then calls this one on behalf of the user.
     */
    private AWSIotMqttClientStatusCallback userStatusCallback;
    /**
     * MQTT subscriptions. Used when resubscribing after a reconnect. Also used
     * to proved per-topic message arrived callbacks.
     */
    private final Map<String, AWSIotMqttTopic> topicListeners;
    /**
     * Queue for messages attempted to publish while MQTT client was offline.
     * Republished upon reconnect.
     */
    private final List<AWSIotMqttQueueMessage> mqttMessageQueue;
    /** KeepAlive interval specified by the user. */
    private int userKeepAlive;
    /** MQTT Will parameters. */
    private AWSIotMqttLastWillAndTestament mqttLWT;
    /** Are we automatically reconnecting upon (non-user) disconnect? */
    private boolean autoReconnect;
    /** Starting time between automatic reconnect attempts.  In seconds. */
    private int minReconnectRetryTime;
    /**
     * Reconnect backoff algorithm maximum time between automatic reconnect attempts. In seconds.
     */
    private int maxReconnectRetryTime;
    /**
     * The current reconnect time in the backoff algorithm.  This will increase
     * as successive reconnects fail.
     */
    private int currentReconnectRetryTime;
    /** Maximum number of automatic reconnect attempts done before giving up. */
    private int maxAutoReconnectAttempts;
    /** Reconnects attempted so far. */
    private int autoReconnectsAttempted;
    /** Is offline publish queueing enabled? */
    private boolean offlinePublishQueueEnabled;
    /** Offline publish queue bound. */
    private Integer offlinePublishQueueBound;
    /** Full queue behavior (keep oldest or keep newest)? */
    private boolean fullQueueKeepsOldest;
    /** Milliseconds between publishes when publishing queued messages (draining interval). */
    private long drainingInterval;
    /** Was this disconnect requested by the user? */
    private boolean userDisconnect;
    /** Do we need to resubscribe upon reconnecting? */
    private boolean needResubscribe;
    /**
     * Holds client socket factory. Set upon initial connect then reused on
     * reconnect.
     */
    private SocketFactory clientSocketFactory;
    /**
     * Holds client provided AWS credentials provider.
     * Set upon initial connect.
     */
    private AWSCredentialsProvider clientCredentialsProvider;

    /** Time to wait after CONNACK to declare the MQTT connection as stable.  In seconds. */
    private Integer connectionStabilityTime;
    /**
     * Timestamp of last successful MQTT connection (MQTT CONNACK).
     * Used to determine connection stability.
     */
    private Long lastConnackTime;
    /** The current connection status of the MQTT client. */
    private MqttManagerConnectionState connectionState;

    /** Override value for System.currentTimeInMillis.  Used for unit testing reconnect logic. */
    private Long unitTestMillisOverride;

    /**
     * Return the customer specific endpoint prefix.
     *
     * @return customer specific endpoint prefix.
     */
    public String getAccountEndpointPrefix() {
        return accountEndpointPrefix;
    }

    /**
     * Is auto-reconnect enabled?
     *
     * @return true if enabled, false if disabled.
     */
    public boolean isAutoReconnect() {
        return autoReconnect;
    }

    /**
     * Enable / disable the auto-reconnect feature.
     *
     * @param enabled true if enabled, false if disabled.
     */
    public void setAutoReconnect(boolean enabled) {
        autoReconnect = enabled;
    }

    /**
     * Return the timeout value between reconnect attempts.
     *
     * @return the auto reconnect timeout value in seconds.
     */
    @Deprecated
    public int getReconnectTimeout() {
        return minReconnectRetryTime;
    }

    /**
     * Sets the timeout value for reconnect attempts.
     *
     * @param timeout timeout value in seconds.
     */
    @Deprecated
    public void setReconnectTimeout(int timeout) {
        this.minReconnectRetryTime = timeout;
    }

    /**
     * Sets the times to wait between autoreconnect attempts.
     * The autoreconnect backoff algorithm starts at the minimum value.
     * For each reconnect attempt that is unsuccessful the reconnect logic
     * will increase the time it waits to attempt the next reconnect.  The
     * maximum value limits the largest retry time.
     *
     * @param minTimeout minimum timeout value in seconds.
     * @param maxTimeout maximum timeout value in seconds.
     */
    public void setReconnectRetryLimits(int minTimeout, int maxTimeout) {
        if (minTimeout > maxTimeout) {
            throw new IllegalArgumentException("Minimum reconnect time needs to be less than Maximum.");
        }

        this.minReconnectRetryTime = minTimeout;
        this.maxReconnectRetryTime = maxTimeout;
    }

    /**
     * Gets the current starting value for time to wait between autoreconnect attempts.
     * @return the auto reconnect timeout value in seconds.
     */
    public int getMinReconnectRetryTime() {
        return minReconnectRetryTime;
    }

    /**
     * Gets the current maximum value for time to wait between autoreconnect attempts.
     * @return the auto reconnect wait time value in seconds.
     */
    public int getMaxReconnectRetryTime() {
        return maxReconnectRetryTime;
    }

    /**
     * Get the current setting of maximum reconnects attempted automatically before quitting.
     * @return number of reconnects to automatically attempt.  Retry forever = -1.
     */
    public int getMaxAutoReconnectAttempts() {
        return maxAutoReconnectAttempts;
    }

    /**
     * Set the maxium reconnects attempted automatically before quitting.
     *
     * @param attempts number of reconnects attempted automatically. Retry
     *            forever = -1.
     */
    public void setMaxAutoReconnectAttepts(int attempts) {
        if (attempts <= 0 && attempts != -1) {
            throw new IllegalArgumentException("Max reconnection attempts must be postive or -1");
        }
        maxAutoReconnectAttempts = attempts;
    }

    /**
     * Sets the connection stability time.  This is the time required to wait after receiving
     * the CONNACK to declare the connection to be stable and established.  When attempting to
     * reconnect after a disconnect the client uses backoff logic to wait longer on each successive
     * unsuccessful attempt.  We reset this logic after a connection has been maintained for the length
     * of connectionStabilityTime without disconnecting.
     * @param time time to wait to declare a connection to be stable (in seconds).
     */
    public void setConnectionStabilityTime(int time) {
        connectionStabilityTime = time;
    }

    /**
     * Gets the connection established time.
     * @return time the client will wait to declare a connection to be stable (in seconds).
     */
    public int getConnectionStabilityTime() {
        return connectionStabilityTime;
    }

    /**
     * Is the publish queue while offline feature enabled?
     *
     * @return boolean if offline queueing is enabled.
     */
    public boolean isOfflinePublishQueueEnabled() {
        return offlinePublishQueueEnabled;
    }

    /**
     * Enable or disable offline publish queueing.
     *
     * @param enabled boolean queueing feature is enabled.
     */
    public void setOfflinePublishQueueEnabled(boolean enabled) {
        offlinePublishQueueEnabled = enabled;
    }

    /**
     * Get the current value of the offline message queue bound.
     *
     * @return max number of messages stored in the message queue.
     */
    public Integer getOfflinePublishQueueBound() {
        return offlinePublishQueueBound;
    }

    /**
     * Set the bound for the number of messages queued while offline. Note: When
     * full queue will act as FIFO and shed oldest messages.
     *
     * @param bound max number of messages to queue while offline. Negative or 0
     *            values ignored.
     */
    public void setOfflinePublishQueueBound(Integer bound) {
        if (bound <= 0) {
            throw new IllegalArgumentException("Offline queue bound must be > 0");
        }
        offlinePublishQueueBound = bound;
    }

    /**
     * Get the "draining interval" (the time between publish messages are sent from the offline queue when reconnected).
     * @return long containing the number of milliseconds between publishes.
     */
    public Long getDrainingInterval() {
        return drainingInterval;
    }

    /**
     * Set the "draining interval" (the time between publish messages are sent from the offline queue when reconnected).
     * @param interval milliseconds between offline queue publishes.
     */
    public void setDrainingInterval(Long interval) {
        drainingInterval = interval;
    }

    /**
     * Keep the oldest messages when publish queue is full?
     * @return boolean true if set to keep oldest messages, false if set to keep newest.
     */
    public boolean fullPublishQueueKeepsOldestMessages() {
        return fullQueueKeepsOldest;
    }

    /**
     * Set the queue behavior on a full queue to keep oldest messages.
     * Default is keep newest.
     */
    public void setFullQueueToKeepOldestMessages() {
        fullQueueKeepsOldest = true;
    }

    /**
     * Set the queue behavior on a full queue to keep newest messages.
     * Default is keep newest.
     */
    public void setFullQueueToKeepNewestMessages() {
        fullQueueKeepsOldest = false;
    }

    /**
     * Get the MQTT keep alive time.
     *
     * @return The MQTT keep alive time set by the user (in seconds).
     */
    public int getKeepAlive() {
        return userKeepAlive;
    }

    /**
     * Sets the MQTT keep alive time used by the underlying MQTT client to
     * determine connection status.
     *
     * @param keepAlive the MQTT keep alive time set by the user (in seconds). A
     *            value of 0 disables keep alive.
     */
    public void setKeepAlive(int keepAlive) {
        if (keepAlive < 0) {
            throw new IllegalArgumentException("Keep alive must be >= 0");
        }
        userKeepAlive = keepAlive;
    }

    /**
     * Get the currently configured Last Will and Testament.
     *
     * @return the current configure LWT.
     */
    public AWSIotMqttLastWillAndTestament getMqttLastWillAndTestament() {
        return mqttLWT;
    }

    /**
     * Set the client last will and testament.
     *
     * @param lwt the desired last will and testament.
     */
    public void setMqttLastWillAndTestament(AWSIotMqttLastWillAndTestament lwt) {
        mqttLWT = lwt;
    }

    /**
     * Set the AWS credentials provider to be used in SigV4 MQTT connections.
     * @param credentialsProvider AWS credentials provider used to create the MQTT connection.
     */
    public void setCredentialsProvider(AWSCredentialsProvider credentialsProvider) {
        clientCredentialsProvider = credentialsProvider;
    }

    /**
     * Sets the MQTT client.  Used for unit tests.
     * @param client - desired MQTT client.
     */
    void setMqttClient(MqttAsyncClient client) {
        mqttClient = client;
    }

    /**
     * Gets offline message queue. Used for unit tests.
     *
     * @return offline message queue.
     */
    List<AWSIotMqttQueueMessage> getMqttMessageQueue() {
        return mqttMessageQueue;
    }

    /**
     * Get MQTT client status.  Used for unit tests.
     * @return mqtt client status.
     */
    MqttManagerConnectionState getConnectionState() {
        return connectionState;
    }

    /**
     * Sets override value for System.currentTimeInMillis.
     * Used for unit testing reconnect logic.
     * @param timeMs desired time returned as milliseconds.
     */
    void setUnitTestMillisOverride(Long timeMs) {
        unitTestMillisOverride = timeMs;
    }

    /**
     * Gets the AWS region.
     * Used for unit tests.
     * @return AWS Region of MQTT Manager.
     */
    Region getRegion() {
        return region;
    }

    /**
     * Get the current system time in milliseconds.
     * Allows for overriding the system time to test the auto reconnect logic.
     * @return a long with the system time in milliseconds or the unit test override.
     */
    private Long getSystemTimeMs() {
        if (unitTestMillisOverride == null) {
            return System.currentTimeMillis();
        } else {
            return unitTestMillisOverride;
        }
    }

    /**
     * Enable/Disable auto resubscribe feature. When enabled, it will automatically
     * resubscribe to previous subscribed topics after abnormal disconnect.
     * By default, this is set to true.
     * @param enabled Indicate whether auto resubscribe feature is enabled.
     */
    public void setAutoResubscribe(boolean enabled) {
        needResubscribe = enabled;
    }

    /**
     * Constructs a new AWSIotMqttManager.
     *
     * @param mqttClientId MQTT client ID to use with this client.
     * @param endpoint AWS IoT endpoint.
     *                 Expected endpoint format: XXXXXX.iot.[region].amazonaws.com
     */
    public AWSIotMqttManager(String mqttClientId, String endpoint) {
        if (mqttClientId == null || mqttClientId.isEmpty()) {
            throw new IllegalArgumentException("mqttClientId is null or empty");
        }

        this.topicListeners = new HashMap<String, AWSIotMqttTopic>();
        this.mqttMessageQueue = new LinkedList<AWSIotMqttQueueMessage>();
        this.accountEndpointPrefix = AwsIotEndpointUtility.getAccountPrefixFromEndpont(endpoint);
        this.mqttClientId = mqttClientId;
        this.region = AwsIotEndpointUtility.getRegionFromIotEndpoint(endpoint);

        initDefaults();
    }

    /**
     * Constructs a new AWSIotMqttManager.
     *
     * @param mqttClientId MQTT client ID to use with this client.
     * @param region The AWS region to use when creating endpoint.
     * @param accountEndpointPrefix Customer specific endpont prefix XXXXXXX in
     *            XXXXXX.iot.[region].amazonaws.com.
     */
    public AWSIotMqttManager(String mqttClientId, Region region, String accountEndpointPrefix) {

        if (mqttClientId == null || mqttClientId.isEmpty()) {
            throw new IllegalArgumentException("mqttClientId is null or empty");
        }

        if (region == null) {
            throw new IllegalArgumentException("region is null");
        }

        if (accountEndpointPrefix == null) {
            throw new IllegalArgumentException("accountEndpointPrefix is null");
        }

        this.topicListeners = new HashMap<String, AWSIotMqttTopic>();
        this.mqttMessageQueue = new LinkedList<AWSIotMqttQueueMessage>();

        this.accountEndpointPrefix = accountEndpointPrefix;
        this.mqttClientId = mqttClientId;
        this.region = region;

        initDefaults();
    }

    /**
     * Initialize client defaults.
     */
    private void initDefaults() {
        connectionState = MqttManagerConnectionState.Disconnected;
        autoReconnect = DEFAULT_AUTO_RECONNECT_ENABLED;
        minReconnectRetryTime = DEFAULT_MIN_RECONNECT_RETRY_TIME_SECONDS;
        maxReconnectRetryTime = DEFAULT_MAX_RECONNECT_RETRY_TIME_SECONDS;
        maxAutoReconnectAttempts = DEFAULT_AUTO_RECONNECT_ATTEMPTS;
        userKeepAlive = DEFAULT_KEEP_ALIVE_SECONDS;
        mqttLWT = null;
        offlinePublishQueueEnabled = DEFAULT_OFFLINE_PUBLISH_QUEUE_ENABLED;
        offlinePublishQueueBound = DEFAULT_OFFLINE_PUBLISH_QUEUE_BOUND;
        drainingInterval = DEFAULT_MILLIS_BETWEEN_QUEUE_PUBLISHES;
        setFullQueueToKeepNewestMessages();
        connectionStabilityTime = DEFAULT_CONNECTION_STABILITY_TIME_SECONDS;
        unitTestMillisOverride = null;
        needResubscribe = true;
    }

    /**
     * Initializes the MQTT session and connects to the specified MQTT server
     * using certificate and private key in keystore. Keystore should be created
     * using IotKeystoreHelper to setup the certificate and key aliases as
     * expected by the underlying socket helper library.
     *
     * @param keyStore A keystore containing an keystore with a certificate and
     *            private key. Use IotKeystoreHelper to get keystore.
     * @param statusCallback When new MQTT session status is received the
     *            function of callback will be called with new connection
     *            status.
     */
    public void connect(KeyStore keyStore, final AWSIotMqttClientStatusCallback statusCallback) {

        if (Build.VERSION.SDK_INT < ANDROID_API_LEVEL_16) {
            throw new UnsupportedOperationException("API Level 16+ required for TLS 1.2 Mutual Auth");
        }

        if (keyStore == null) {
            throw new IllegalArgumentException("keyStore is null");
        }

        this.userStatusCallback = statusCallback;

        // Do nothing if Connecting, Connected or Reconnecting
        if (connectionState != MqttManagerConnectionState.Disconnected) {
            userConnectionCallback();
            return;
        }

        mqttBrokerURL = String.format("ssl://%s.iot.%s.%s:8883", accountEndpointPrefix, region.getName(),
                region.getDomain());
        isWebSocketClient = false;
        LOGGER.debug("MQTT broker: " + mqttBrokerURL);

        try {
            if (mqttClient == null) {
                mqttClient = new MqttAsyncClient(mqttBrokerURL, mqttClientId, new MemoryPersistence());
            }

            final SocketFactory socketFactory = AWSIotSslUtility.getSocketFactoryWithKeyStore(keyStore);
            final MqttConnectOptions options = new MqttConnectOptions();

            if (mqttLWT != null) {
                options.setWill(mqttLWT.getTopic(), mqttLWT.getMessage().getBytes(), mqttLWT.getQos().asInt(),
                        false);
            }

            clientSocketFactory = socketFactory;
            options.setSocketFactory(clientSocketFactory);

            mqttConnect(options, statusCallback);
        } catch (final NoSuchAlgorithmException e) {
            throw new AWSIotCertificateException("A certificate error occurred.", e);
        } catch (final KeyManagementException e) {
            throw new AWSIotCertificateException("A certificate error occurred.", e);
        } catch (final KeyStoreException e) {
            throw new AWSIotCertificateException("A certificate error occurred.", e);
        } catch (final UnrecoverableKeyException e) {
            throw new AWSIotCertificateException("A certificate error occurred.", e);
        } catch (final MqttException e) {
            throw new AmazonClientException("An error occured in the MQTT client.", e);
        }
    }

    /**
     * Initializes the MQTT session and connects to the specified MQTT server
     * using AWS credentials.
     *
     * @param credentialsProvider AWS credentialsProvider used to create a WebSocket connection to AWS IoT.
     * @param statusCallback When new MQTT session status is received the function of callback will
     *                       be called with new connection status.
     */
    public void connect(AWSCredentialsProvider credentialsProvider,
            final AWSIotMqttClientStatusCallback statusCallback) {
        clientCredentialsProvider = credentialsProvider;

        if (credentialsProvider == null) {
            throw new IllegalArgumentException("credentials provider cannot be null");
        }

        this.userStatusCallback = statusCallback;

        // Do nothing if Connecting, Connected or Reconnecting
        if (connectionState != MqttManagerConnectionState.Disconnected) {
            userConnectionCallback();
            return;
        }

        // create a thread as the credentials provider getCredentials() call may require
        // a network call and will possibly block this connect() call
        new Thread(new Runnable() {
            @Override
            public void run() {

                signer = new AWSIotWebSocketUrlSigner("iotdata");

                final String endpoint = String.format("%s.iot.%s.%s:443", accountEndpointPrefix, region.getName(),
                        region.getDomain());
                isWebSocketClient = true;
                LOGGER.debug("MQTT broker: " + endpoint);
                try {
                    final String mqttWebSocketURL = signer.getSignedUrl(endpoint,
                            clientCredentialsProvider.getCredentials(), System.currentTimeMillis());

                    final MqttConnectOptions options = new MqttConnectOptions();

                    // Specify the URL through the server URI array.  This is checked
                    // at connect time and allows us to specify a new URL (with new
                    // SigV4 parameters) for each connect.
                    options.setServerURIs(new String[] { mqttWebSocketURL });

                    if (mqttLWT != null) {
                        options.setWill(mqttLWT.getTopic(), mqttLWT.getMessage().getBytes(),
                                mqttLWT.getQos().asInt(), false);
                    }

                    if (mqttClient == null) {
                        mqttClient = new MqttAsyncClient("wss://" + endpoint, mqttClientId,
                                new MemoryPersistence());
                    }
                    mqttConnect(options, statusCallback);

                } catch (final MqttException e) {
                    throw new AmazonClientException("An error occurred in the MQTT client.", e);
                }
            }
        }, "Mqtt Connect Thread").start();
    }

    /**
     * Connect to the MQTT service.
     *
     * @param options        MQTT connect options containing a TLS socket factory for authentication.
     * @param statusCallback callback for status updates on connection.
     */
    private void mqttConnect(MqttConnectOptions options, final AWSIotMqttClientStatusCallback statusCallback) {
        LOGGER.debug("ready to do mqtt connect");

        // AWS IoT does not currently support persistent sessions
        options.setCleanSession(true);
        options.setKeepAliveInterval(userKeepAlive);

        topicListeners.clear();
        mqttMessageQueue.clear();

        resetReconnect();
        userDisconnect = false;

        setupCallbackForMqttClient();

        try {
            connectionState = MqttManagerConnectionState.Connecting;
            userConnectionCallback();

            mqttClient.connect(options, null, new IMqttActionListener() {
                @Override
                public void onSuccess(IMqttToken asyncActionToken) {
                    LOGGER.info("onSuccess: mqtt connection is successful.");
                    connectionState = MqttManagerConnectionState.Connected;

                    lastConnackTime = getSystemTimeMs();

                    if (mqttMessageQueue.size() > 0) {
                        publishMessagesFromQueue();
                    }

                    userConnectionCallback();
                }

                @Override
                public void onFailure(IMqttToken asyncActionToken, Throwable e) {
                    LOGGER.warn("onFailure: connection failed.");
                    // Testing shows following reason codes:
                    // REASON_CODE_CLIENT_EXCEPTION = network unavailable / host unresolved
                    // REASON_CODE_CLIENT_EXCEPTION = deactivated certificate
                    // REASON_CODE_CLIENT_EXCEPTION = unknown certificate
                    // REASON_CODE_CONNECTION_LOST = no policy attached
                    // REASON_CODE_CONNECTION_LOST = bad connection policy

                    if (!userDisconnect && autoReconnect) {
                        connectionState = MqttManagerConnectionState.Reconnecting;
                        userConnectionCallback();
                        scheduleReconnect();
                    } else {
                        connectionState = MqttManagerConnectionState.Disconnected;
                        userConnectionCallback(e);
                    }
                }
            });
        } catch (final MqttException e) {
            switch (e.getReasonCode()) {
            case MqttException.REASON_CODE_CLIENT_CONNECTED:
                connectionState = MqttManagerConnectionState.Connected;
                userConnectionCallback();
                break;
            case MqttException.REASON_CODE_CONNECT_IN_PROGRESS:
                connectionState = MqttManagerConnectionState.Connecting;
                userConnectionCallback();
                break;
            default:
                connectionState = MqttManagerConnectionState.Disconnected;
                userConnectionCallback(e);
                break;
            }
        }
    }

    /**
     * Disconnect from a mqtt client (close current MQTT session).
     *
     * @return true if disconnect finished with success.
     */
    public boolean disconnect() {
        userDisconnect = true;
        reset();
        topicListeners.clear();
        connectionState = MqttManagerConnectionState.Disconnected;
        userConnectionCallback();
        return true;
    }

    /**
     * Disconnect the MQTT client. Issues a disconnect request if the client is
     * connected.
     */
    void reset() {
        if (null != mqttClient) {
            if (mqttClient.isConnected()) {
                try {
                    mqttClient.disconnect(0);
                } catch (final MqttException e) {
                    throw new AmazonClientException("Client error when disconnecting.", e);
                }
            }
        }
    }

    /**
     * Reconnect to MQTT broker.
     * Attempts to reconnect.  If unsuccessful schedules a reconnect attempt.
     */
    void reconnectToSession() {
        // status will be ConnectionLost if user calls disconnect() during reconnect logic
        if (null != mqttClient && connectionState != MqttManagerConnectionState.Disconnected) {
            LOGGER.info("attempting to reconnect to mqtt broker");

            final MqttConnectOptions options = new MqttConnectOptions();

            options.setCleanSession(true);
            options.setKeepAliveInterval(userKeepAlive);

            if (mqttLWT != null) {
                options.setWill(mqttLWT.getTopic(), mqttLWT.getMessage().getBytes(), mqttLWT.getQos().asInt(),
                        false);
            }

            if (isWebSocketClient) {
                signer = new AWSIotWebSocketUrlSigner("iotdata");

                final String endpoint = String.format("%s.iot.%s.%s:443", accountEndpointPrefix, region.getName(),
                        region.getDomain());
                try {
                    final String mqttWebSocketURL = signer.getSignedUrl(endpoint,
                            clientCredentialsProvider.getCredentials(), System.currentTimeMillis());
                    LOGGER.debug(
                            "Reconnect to mqtt broker: " + endpoint + " mqttWebSocketURL: " + mqttWebSocketURL);
                    // Specify the URL through the server URI array.  This is checked
                    // at connect time and allows us to specify a new URL (with new
                    // SigV4 parameters) for each connect.
                    options.setServerURIs(new String[] { mqttWebSocketURL });
                } catch (AmazonClientException e) {
                    LOGGER.error("Failed to get credentials. AmazonClientException: ", e);
                    //TODO: revisit how to handle exception thrown by getCredentials() properly.
                    if (scheduleReconnect()) {
                        connectionState = MqttManagerConnectionState.Reconnecting;
                    } else {
                        connectionState = MqttManagerConnectionState.Disconnected;
                    }
                    userConnectionCallback();
                }
            } else {
                options.setSocketFactory(clientSocketFactory);
            }

            setupCallbackForMqttClient();
            try {
                ++autoReconnectsAttempted;
                LOGGER.debug("mqtt reconnecting attempt " + autoReconnectsAttempted);
                mqttClient.connect(options, null, new IMqttActionListener() {
                    @Override
                    public void onSuccess(IMqttToken asyncActionToken) {
                        LOGGER.info("Reconnect successful");
                        connectionState = MqttManagerConnectionState.Connected;

                        lastConnackTime = getSystemTimeMs();

                        if (needResubscribe) {
                            resubscribeToTopics();
                        }
                        if (mqttMessageQueue.size() > 0) {
                            publishMessagesFromQueue();
                        }

                        userConnectionCallback();
                    }

                    @Override
                    public void onFailure(IMqttToken asyncActionToken, Throwable e) {
                        LOGGER.warn("Reconnect failed ");
                        if (scheduleReconnect()) {
                            connectionState = MqttManagerConnectionState.Reconnecting;
                            userConnectionCallback();
                        } else {
                            connectionState = MqttManagerConnectionState.Disconnected;
                            userConnectionCallback();
                        }
                    }
                });
            } catch (final MqttException e) {
                LOGGER.error("Exception during reconnect, exception: ", e);
                if (scheduleReconnect()) {
                    connectionState = MqttManagerConnectionState.Reconnecting;
                    userConnectionCallback();
                } else {
                    connectionState = MqttManagerConnectionState.Disconnected;
                    userConnectionCallback(e);
                }
            }
        }
    }

    /**
     * Schedule an auto-reconnect attempt using backoff logic.
     *
     * @return true if attempt was scheduled, false otherwise.
     */
    private boolean scheduleReconnect() {
        LOGGER.info("schedule Reconnect attempt " + autoReconnectsAttempted + " of " + maxAutoReconnectAttempts
                + " in " + currentReconnectRetryTime + " seconds.");
        // schedule a reconnect if unlimited or if we haven't yet hit the limit

        if (maxAutoReconnectAttempts == -1 || autoReconnectsAttempted < maxAutoReconnectAttempts) {
            //Start a separate thread to do reconnect, because connection must not occur on the main thread.
            final HandlerThread ht = new HandlerThread("Reconnect thread");
            ht.start();
            Looper looper = ht.getLooper();
            Handler handler = new Handler(looper);
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    LOGGER.debug("TID: " + ht.getThreadId() + " trying to reconnect to session");
                    if (mqttClient != null && !mqttClient.isConnected()) {
                        reconnectToSession();
                    }
                }
            }, MILLIS_IN_ONE_SECOND * currentReconnectRetryTime);
            currentReconnectRetryTime = Math.min(currentReconnectRetryTime * 2, maxReconnectRetryTime);
            return true;
        } else {
            LOGGER.warn("schedule reconnect returns false");
            return false;
        }
    }

    /**
     * Reset the backoff logic to the initial values.
     */
    public void resetReconnect() {
        LOGGER.info("resetting reconnect attempt and retry time");
        autoReconnectsAttempted = 0;
        currentReconnectRetryTime = minReconnectRetryTime;
    }

    /**
     * Subscribes to an MQTT topic.
     *
     * @param topic The topic to which to subscribe.
     * @param qos Quality of Service Level of the subscription.
     * @param callback Callback to be called when new message is received on
     *            this topic for this subscription.
     */
    public void subscribeToTopic(String topic, AWSIotMqttQos qos, AWSIotMqttNewMessageCallback callback) {

        if (topic == null || topic.isEmpty()) {
            throw new IllegalArgumentException("topic is null or empty");
        }

        if (qos == null) {
            throw new IllegalArgumentException("QoS cannot be null.");
        }

        if (null != mqttClient) {
            try {
                mqttClient.subscribe(topic, qos.asInt());
            } catch (final MqttException e) {
                throw new AmazonClientException("Client error when subscribing.", e);
            }
            final AWSIotMqttTopic topicModel = new AWSIotMqttTopic(topic, qos, callback);
            topicListeners.put(topic, topicModel);
        }
    }

    /**
     * Unsubscribe from an MQTT topic.
     *
     * @param topic topic from which to unsubscribe.
     */
    public void unsubscribeTopic(String topic) {

        if (topic == null || topic.isEmpty()) {
            throw new IllegalArgumentException("topic is null or empty");
        }

        if (mqttClient != null) {
            try {
                mqttClient.unsubscribe(topic);
            } catch (final MqttException e) {
                throw new AmazonClientException("Client error while unsubscribing.", e);
            }
            topicListeners.remove(topic);
        }
    }

    /**
     * Resubscribe to previously subscribed topics on reconnecting.
     */
    void resubscribeToTopics() {
        LOGGER.info("Auto-resubscribe is enabled. Resubscribing to previous topics.");
        for (final AWSIotMqttTopic topic : topicListeners.values()) {
            if (mqttClient != null) {
                try {
                    mqttClient.subscribe(topic.getTopic(), topic.getQos().asInt());
                } catch (final MqttException e) {
                    LOGGER.error("Error while resubscribing to previously subscribed toipcs.", e);
                }
            }
        }
    }

    /**
     * Send a message to an MQTT topic.
     *
     * @param str The message payload to be sent (as a String).
     * @param topic The topic on which to publish.
     * @param qos The quality of service requested for this message.
     */
    public void publishString(String str, String topic, AWSIotMqttQos qos) {

        if (str == null) {
            throw new IllegalArgumentException("publish string is null");
        }

        if (topic == null || topic.isEmpty()) {
            throw new IllegalArgumentException("topic is null or empty");
        }

        if (qos == null) {
            throw new IllegalArgumentException("QoS cannot be null");
        }

        publishData(str.getBytes(StringUtils.UTF8), topic, qos);
    }

    /**
     * Send a message to an MQTT topic.
     *
     * @param str The message payload to be sent (as a String).
     * @param topic The topic on which to publish.
     * @param qos The quality of service requested for this message.
     * @param cb Callback for message status.
     * @param userData User defined data which will be passed back to the user when the
     *                 callback is invoked.
     */
    public void publishString(String str, String topic, AWSIotMqttQos qos, AWSIotMqttMessageDeliveryCallback cb,
            Object userData) {

        if (str == null) {
            throw new IllegalArgumentException("publish string is null");
        }

        if (topic == null || topic.isEmpty()) {
            throw new IllegalArgumentException("topic is null or empty");
        }

        if (qos == null) {
            throw new IllegalArgumentException("QoS cannot be null");
        }

        publishData(str.getBytes(StringUtils.UTF8), topic, qos, cb, userData);
    }

    /**
     * Publish data to an MQTT topic.
     *
     * @param data The message payload to be sent as a byte array.
     * @param topic The topic on which to publish.
     * @param qos The quality of service requested for this message.
     */
    public void publishData(byte[] data, String topic, AWSIotMqttQos qos) {

        publishData(data, topic, qos, null, null);
    }

    /**
     * Publish data to an MQTT topic.
     *
     * @param data The message payload to be sent as a byte array.
     * @param topic The topic on which to publish.
     * @param qos The quality of service requested for this message.
     * @param callback Callback for message status.
     * @param userData User defined data which will be passed back to the user when the
     *                 callback is invoked.
     */
    public void publishData(byte[] data, String topic, AWSIotMqttQos qos,
            AWSIotMqttMessageDeliveryCallback callback, Object userData) {

        if (topic == null || topic.isEmpty()) {
            throw new IllegalArgumentException("topic is null or empty");
        }

        if (data == null) {
            throw new IllegalArgumentException("data is null");
        }

        if (qos == null) {
            throw new IllegalArgumentException("QoS cannot be null");
        }

        final PublishMessageUserData publishMessageUserData = new PublishMessageUserData(callback, userData);

        if (connectionState == MqttManagerConnectionState.Connected) {
            if (mqttMessageQueue.isEmpty()) {
                try {
                    mqttClient.publish(topic, data, qos.asInt(), false, publishMessageUserData, null);
                } catch (final MqttException e) {
                    if (callback != null) {
                        userPublishCallback(callback, AWSIotMqttMessageDeliveryCallback.MessageDeliveryStatus.Fail,
                                userData);
                    } else {
                        // throw an exception on publish error if the user did not set a callback
                        throw new AmazonClientException("Client error while publishing.", e);
                    }
                }
            } else {
                // if the queue has messages we're making the assumption that offline queueing is enabled
                if (!putMessageInQueue(data, topic, qos, publishMessageUserData)) {
                    // queue is full (and set to hold onto the oldest messages)
                    userPublishCallback(callback, AWSIotMqttMessageDeliveryCallback.MessageDeliveryStatus.Fail,
                            userData);
                }
            }
        } else if (connectionState == MqttManagerConnectionState.Reconnecting) {
            if (offlinePublishQueueEnabled) {
                if (!putMessageInQueue(data, topic, qos, publishMessageUserData)) {
                    // queue is full (and set to hold onto the oldest messages)
                    userPublishCallback(callback, AWSIotMqttMessageDeliveryCallback.MessageDeliveryStatus.Fail,
                            userData);
                }
            }
        } else {
            throw new AmazonClientException("Client is disconnected or not yet connected.");
        }
    }

    /**
     * Add a message to the publishing queue. A publish call adds to the queue
     * if the client is unable to publish (offline).
     * Behavior on a full queue is defined by fullQueueKeepsOldest.  If this is true
     * we keep the oldest values so we skip adding on a full queue.  If this is false
     * we want the queue to always have the latest values so pop the first element out
     * and append.
     *
     * @param data  byte array of message payload.
     * @param topic message topic.
     * @param qos   The quality of service requested for this message.
     * @param publishMessageUserData The user supplied data for this message including a
     *                               callback and context.
     * @return True if message is enqueued, false if queue is full and queue is set to skip on full.
     */
    boolean putMessageInQueue(byte[] data, String topic, AWSIotMqttQos qos,
            PublishMessageUserData publishMessageUserData) {
        final AWSIotMqttQueueMessage message = new AWSIotMqttQueueMessage(topic, data, qos, publishMessageUserData);

        if (mqttMessageQueue.size() >= offlinePublishQueueBound) {
            if (fullQueueKeepsOldest) {
                return false;
            } else {
                mqttMessageQueue.remove(0);
            }
        }

        mqttMessageQueue.add(message);
        return true;
    }

    /**
     * Publish messages from the message queue.
     * Called to handle publishing messages accumulated in the message queue when the client was unable to publish.
     */
    void publishMessagesFromQueue() {
        if (connectionState == MqttManagerConnectionState.Connected && mqttMessageQueue != null
                && !mqttMessageQueue.isEmpty()) {
            final AWSIotMqttQueueMessage message = mqttMessageQueue.remove(0);
            if (message != null) {
                try {
                    if (message.getUserData() != null && message.getUserData().getUserCallback() != null) {
                        // this queued message has a callback, publish passing the user data
                        mqttClient.publish(message.getTopic(), message.getMessage(), message.getQos().asInt(),
                                false, message.getUserData(), null);
                    } else {
                        // this queued message does not have a callback
                        mqttClient.publish(message.getTopic(), message.getMessage(), message.getQos().asInt(),
                                false);
                    }
                } catch (final MqttException e) {
                    // Call this message a failure.  It is possible that this is due to a
                    // connection issue (we are in this path because the connection dropped),
                    // however there are also exceptions inherent to the message (valid topic),
                    // such that publishing this message would never succeed.  It is safer to
                    // remove the message from the queue and notify failure than to block
                    // the queue indefinitely.
                    userPublishCallback(message.getUserData().getUserCallback(),
                            AWSIotMqttMessageDeliveryCallback.MessageDeliveryStatus.Fail,
                            message.getUserData().getUserData());
                }
            }

            (new Handler(Looper.getMainLooper())).postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (!mqttMessageQueue.isEmpty()) {
                        if (connectionState == MqttManagerConnectionState.Connected) {
                            publishMessagesFromQueue();
                        }
                    }
                }
            }, drainingInterval);
        }
    }

    /**
     * Setup the MQTT client calbacks. The Paho MQTT client exposes callbacks
     * for connection status, publish status and incoming messages. The Android
     * MQTT client uses the single incoming message callback to map to per-topic
     * callbacks.
     */
    void setupCallbackForMqttClient() {
        LOGGER.debug("Setting up Callback for MqttClient");
        mqttClient.setCallback(new MqttCallback() {
            @Override
            public void connectionLost(Throwable cause) {
                LOGGER.warn("connection is Lost");
                if (!userDisconnect && autoReconnect) {
                    connectionState = MqttManagerConnectionState.Reconnecting;
                    userConnectionCallback();

                    // If we have been connected longer than the connectionStabilityTime then
                    // restart the reconnect logic from minimum value before scheduling reconnect.
                    if ((lastConnackTime + (connectionStabilityTime * MILLIS_IN_ONE_SECOND)) < getSystemTimeMs()) {
                        resetReconnect();
                    }
                    scheduleReconnect();
                } else {
                    connectionState = MqttManagerConnectionState.Disconnected;
                    userConnectionCallback(cause);
                }
            }

            @Override
            public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
                LOGGER.info("message arrived on topic: " + topic);
                final byte[] data = mqttMessage.getPayload();

                for (final String topicKey : topicListeners.keySet()) {
                    if (isTopicMatch(topicKey, topic)) {
                        final AWSIotMqttTopic topicModel = topicListeners.get(topicKey);
                        if (topicModel != null) {
                            if (topicModel.getCallback() != null) {
                                topicModel.getCallback().onMessageArrived(topic, data);
                            }
                        }
                    }
                }
            }

            @Override
            public void deliveryComplete(IMqttDeliveryToken token) {
                LOGGER.info("delivery is complete");
                if (token != null) {
                    final Object o = token.getUserContext();
                    if (o instanceof PublishMessageUserData) {
                        final PublishMessageUserData pmud = (PublishMessageUserData) o;
                        userPublishCallback(pmud.getUserCallback(),
                                AWSIotMqttMessageDeliveryCallback.MessageDeliveryStatus.Success,
                                pmud.getUserData());
                    }
                }
            }
        });
    }

    /**
     * Is the MQTT client ready to publish messages? (Created and connected).
     *
     * @return true equals ready to publish, false equals offline.
     */
    boolean isReadyToPublish() {
        return mqttClient != null && mqttClient.isConnected();
    }

    /**
     * Wrapper method to notify user if they have specified a callback.
     * Maps internal state to user connection statuses.
     */
    void userConnectionCallback() {
        userConnectionCallback(null);
    }

    /**
     * Wrapper method to notify user if they have specified a callback.
     * Maps internal state to user connection statuses.
     * @param t a Throwable that may have originated from the status change.
     */
    void userConnectionCallback(Throwable t) {
        if (userStatusCallback != null) {
            switch (connectionState) {
            case Connected:
                userStatusCallback.onStatusChanged(AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.Connected,
                        t);
                break;
            case Connecting:
                userStatusCallback.onStatusChanged(AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.Connecting,
                        t);
                break;
            case Reconnecting:
                userStatusCallback
                        .onStatusChanged(AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.Reconnecting, t);
                break;
            case Disconnected:
                userStatusCallback
                        .onStatusChanged(AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.ConnectionLost, t);
                break;
            default:
                throw new IllegalStateException("Unknown connection state.");
            }
        }
    }

    /**
     * Convenience wrapper method to notify user if they have specified a callback.
     * @param cb - callback to be invoked.
     * @param status - message status to pass in the callback.
     * @param userData User defined data to be passed to the user in the callback.
     */
    void userPublishCallback(AWSIotMqttMessageDeliveryCallback cb,
            AWSIotMqttMessageDeliveryCallback.MessageDeliveryStatus status, Object userData) {
        if (cb != null) {
            cb.statusChanged(status, userData);
        }
    }

    /**
     * Does the topic match the topic filter?
     *
     * @param topicFilter MQTT topic filter (subscriptions, including
     *            wildcards).
     * @param topic - the aboslute topic (no wildcards) on which a message was
     *            published.
     * @return true if the topic matches the filter, false otherwise.
     */
    static boolean isTopicMatch(String topicFilter, String topic) {
        final String[] topicFilterTokens = topicFilter.split("/");
        final String[] topicTokens = topic.split("/");

        if (topicFilterTokens.length > topicTokens.length) {
            return false;
        }

        for (int i = 0; i < topicFilterTokens.length; i++) {
            final String topicFilterToken = topicFilterTokens[i];
            final String topicToken = topicTokens[i];

            // we're equal up to this point, the # matches all that is left
            if (("#").equals(topicFilterToken)) {
                return true;
            }

            // if the filter has a +, go to the next token
            // if the filter token matches the topic token, go to the next token
            // if neither are true then we've discovered a mismatch
            if (!("+").equals(topicFilterToken) && !topicFilterToken.equals(topicToken)) {
                return false;
            }
        }

        // if we're here the strings matched token-for-token (including any
        // '+'s)
        // and we have reached the end of the topic filter
        // we have a match unless there are more tokens left in the topic
        // if we have filter 1/2/3 then:
        // 1/2/3 matches
        // 1/2/3/4 does not match
        //
        // if we have filter 1/2/+ then:
        // 1/2/3 matches
        // 1/2/3/4 does not match
        //
        // we already know the filter can't be longer than the topic (from
        // above)
        return (topicFilterTokens.length == topicTokens.length);
    }
}