com.magnet.mmx.client.MMXClient.java Source code

Java tutorial

Introduction

Here is the source code for com.magnet.mmx.client.MMXClient.java

Source

/*   Copyright (c) 2015 Magnet Systems, Inc.
 *
 *  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://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License 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.magnet.mmx.client;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.location.Location;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.gcm.GoogleCloudMessaging;
import com.google.android.gms.location.LocationServices;
import com.magnet.mmx.client.common.*;
import com.magnet.mmx.protocol.AuthData;
import com.magnet.mmx.protocol.CarrierEnum;
import com.magnet.mmx.protocol.Constants;
import com.magnet.mmx.protocol.DevReg;
import com.magnet.mmx.protocol.GeoLoc;
import com.magnet.mmx.protocol.MMXError;
import com.magnet.mmx.protocol.MMXStatus;
import com.magnet.mmx.protocol.MMXTopic;
import com.magnet.mmx.protocol.OSType;
import com.magnet.mmx.protocol.PushType;
import com.magnet.mmx.util.DefaultEncryptor;

import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
import org.jivesoftware.smack.SmackAndroid;

import java.io.IOException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.net.SocketFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

/**
 * The primary entry point for interacting with MMX.  The named MMXClient instances are
 * managed by this class.  In most cases, when retrieving an MMXClient instance with getInstance()
 * a name doesn't need to be provided. There are two ways to configure the MMXClient instance.
 *
 * 1.  Using the properties file in res/raw and providing the getInstance() method with the id
 * 2.  Implementing an MMXClientConfig class and passing it to the getInstance() method.
 */
public final class MMXClient {
    private static final String TAG = MMXClient.class.getSimpleName();
    private static final char[] USERNAME_INVALID_CHARS = { '%', '/', '@', '&' };
    private static final int USERNAME_LENGTH_MIN = 1;
    private static final int USERNAME_LENGTH_MAX = 40;
    private static final String USERNAME_INVALID_CHARS_STR;

    static {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < USERNAME_INVALID_CHARS.length; i++) {
            sb.append(USERNAME_INVALID_CHARS[i]);
            if (i < USERNAME_INVALID_CHARS.length - 1) {
                sb.append(',');
            }
        }
        USERNAME_INVALID_CHARS_STR = sb.toString();
    }

    /**
     * Options to specify for a connection.  By default, this class will specify
     * <code>
     * autoCreate = false
     * suspendDelivery = false
     * </code>
     *
     * This means that in the default case, the user will NOT be auto-created if they don't
     * exist and messages will delivered to the callback upon connecting.
     */
    public static final class ConnectionOptions {
        private boolean mAutoCreate = false;
        private boolean mSuspendDelivery = false;

        //These are used in MMXClient internally and may be exposed in the future
        private boolean mAnonymous = false;
        private String mUsername = null;
        private String mPassword = null;

        /**
         * The auto-creation flag.
         *
         * @return true if the user will be auto-created
         */
        public boolean isAutoCreate() {
            return mAutoCreate;
        }

        /**
         * Set the auto-create flag for this options object.
         *
         * @param autoCreate true if the user should be auto-created, false otherwise
         * @return this builder object
         */
        public ConnectionOptions setAutoCreate(boolean autoCreate) {
            mAutoCreate = autoCreate;
            return this;
        }

        /**
         * The current state for the "suspend delivery" option.
         *
         * @return true if delivery will be suspended upon connect
         * @see MMXClient#resumeDelivery()
         */
        public boolean isSuspendDelivery() {
            return mSuspendDelivery;
        }

        /**
         * Set the suspend delivery flag for this options object.  If true, messages will not be delivered
         * to the callback until resumeDelivery() is called.
         *
         * @param suspendDelivery true if message delivery should be suspended upon connecting, false otherwise
         * @return this builder object
         * @see com.magnet.mmx.client.MMXClient#resumeDelivery()
         */
        public ConnectionOptions setSuspendDelivery(boolean suspendDelivery) {
            mSuspendDelivery = suspendDelivery;
            return this;
        }

        private boolean isAnonymous() {
            return mAnonymous;
        }

        private ConnectionOptions setAnonymous(boolean anonymous) {
            mAnonymous = anonymous;
            return this;
        }

        private String getUsername() {
            return mUsername;
        }

        private ConnectionOptions setUsername(String username) {
            mUsername = username;
            return this;
        }

        private String getPassword() {
            return mPassword;
        }

        private ConnectionOptions setPassword(String password) {
            mPassword = password;
            return this;
        }

        protected ConnectionOptions clone() {
            return new ConnectionOptions().setSuspendDelivery(mSuspendDelivery).setAutoCreate(mAutoCreate)
                    .setAnonymous(mAnonymous).setUsername(mUsername).setPassword(mPassword);
        }
    }

    /**
     * The enumeration of events that will cause MMXListener.onConnectionEvent() to be invoked.
     */
    public enum ConnectionEvent {
        /**
         * Successfully connected
         */
        CONNECTED,
        /**
         * Authentication failed
         */
        AUTHENTICATION_FAILURE,
        /**
         * Unable to connect to the server
         */
        CONNECTION_FAILED,
        /**
         * Disconnected from server
         */
        DISCONNECTED,
        /**
         * Registration with the wakeup service failed (GCM)
         */
        WAKEUP_REGISTRATION_FAILED
    }

    /**
     * The various security levels.
     */
    public enum SecurityLevel {
        /**
         * No security.  This will not force TLS.
         */
        NONE,

        /**
         * Strict security.  Forces TLS and valid SSL certs/hostnames
         */
        STRICT,

        /**
         * Relaxed security.  Forces TLS, but allows self-signed certs and invalid hostnames
         */
        RELAXED
    }

    private static HashMap<String, MMXClient> sInstanceMap = new HashMap<String, MMXClient>();
    private static final String DEFAULT_MMX_NAME = "__DEFAULT__";
    private static final int TCP_CONNECTION_TIMEOUT = 7000;
    private static final String SHARED_PREF_NAME = "com.magnet.mmx.MMXClient-";
    private static final String SHARED_PREF_KEY_GCM_REGID = "GCM_REGID";
    private static final String SHARED_PREF_KEY_GCM_REGID_APPVERSION = "GCM_REGID_APPVERSION";
    private static final String SHARED_PREF_KEY_GCM_WAKEUP_ENABLED = "WAKEUP_ENABLED";
    private static final String SHARED_PREF_KEY_AUTH_MODE = "AUTH_MODE";
    private static final String SHARED_PREF_KEY_SECURITY_LEVEL = "SECURITY_LEVEL";

    //config items
    private static final String SHARED_PREF_KEY_CONFIG_HOST = "HOST";
    private static final String SHARED_PREF_KEY_CONFIG_PORT = "PORT";
    private static final String SHARED_PREF_KEY_CONFIG_GCM_PROJECTID = "GCM_PROJECTID";
    private static final String SHARED_PREF_KEY_CONFIG_APP_ID = "APP_ID";
    private static final String SHARED_PREF_KEY_CONFIG_API_KEY = "API_KEY";
    private static final String SHARED_PREF_KEY_CONFIG_SERVER_USER = "SERVER_USER";
    private static final String SHARED_PREF_KEY_CONFIG_ANONYMOUS_SECRET = "ANONYMOUS_SECRET";
    private static final String SHARED_PREF_KEY_CONFIG_DOMAIN_NAME = "DOMAIN_NAME";
    private static final String SHARED_PREF_KEY_CONFIG_DEBUG_DEVICE_ID = "DEBUG_DEVICE_ID";

    //There is only one registered wakeup listener for all instances.
    private static final String STATIC_SHARED_PREF_NAME = MMXClient.class.getSimpleName();
    private static final String STATIC_SHARED_PREF_KEY_WAKEUP_LISTENER_CLASS = "WAKEUP_LISTENER_CLASS";
    private static final String STATIC_SHARED_PREF_KEY_WAKEUP_INTERVAL = "WAKEUP_INTERVAL";

    /**
     * Intent used to invoke the MMXClient.MMXWakeupListener during the polling interval. See {@link #setWakeupInterval(Context, long)}
     */
    public static final String ACTION_WAKEUP = "com.magnet.mmx.MMXClient.WAKEUP";
    /**
     * Intent used to invoke the MMXClient.MMXWakeupListener when a new incoming message is queued on the MMX server
     */
    public static final String ACTION_RETRIEVE_MESSAGES = "com.magnet.mmx.MMXClient.RETRIEVE_MESSAGES";
    /**
     * Intent used to invoke the MMXClient.MMXWakeupListener when a push notification is received
     */
    public static final String ACTION_PUSH_RECEIVED = "com.magnet.mmx.MMXClient.PUSH_RECEIVED";

    /**
     * Extra String field that contains the unique push id from push{@link #ACTION_PUSH_RECEIVED}
     */
    public static final String EXTRA_PUSH_ID = "com.magnet.mmx.MMXClient.EXTRA_PUSH_ID";
    /**
     * Extra String field that contains the title text from push{@link #ACTION_PUSH_RECEIVED}
     */
    public static final String EXTRA_PUSH_TITLE = "com.magnet.mmx.MMXClient.EXTRA_PUSH_TITLE";
    /**
     * Extra String field that contains the body text from push{@link #ACTION_PUSH_RECEIVED}
     */
    public static final String EXTRA_PUSH_BODY = "com.magnet.mmx.MMXClient.EXTRA_PUSH_BODY";
    /**
     * Extra String field that contains the sound name from push{@link #ACTION_PUSH_RECEIVED}
     */
    public static final String EXTRA_PUSH_SOUND = "com.magnet.mmx.MMXClient.EXTRA_PUSH_SOUND";
    /**
     * Extra String field that contains the icon name from push{@link #ACTION_PUSH_RECEIVED}
     */
    public static final String EXTRA_PUSH_ICON = "com.magnet.mmx.MMXClient.EXTRA_PUSH_ICON";
    /**
     * Extra String field that contains the custom json block from push{@link #ACTION_PUSH_RECEIVED}
     */
    public static final String EXTRA_PUSH_CUSTOM_JSON = "com.magnet.mmx.MMXClient.EXTRA_PUSH_CUSTOM";

    final String mName;
    private final Context mContext;
    private SharedPreferences mSharedPreferences = null;
    private MMXConnection mConnection = null;
    private MMXContext mMMXContext = null;
    private MMXSettings mSettings = null;
    private ConnectionOptions mConnectionOptions = null;
    private MMXListener mMMXListener = null;
    private HandlerThread mMessagingThread = null;
    private Handler mMessagingHandler = null;
    private ConnectionInfo mConnectionInfo = null;
    private PersistentQueue mQueue = null;

    //managers
    private HashMap<Class, MMXManager> mManagers = new HashMap<Class, MMXManager>();

    private byte[] mEncryptionKey = new byte[256 / 8];
    private String mEncryptionString = "zpdi3901!)9" + "39v91a{F{#" + ">@['d.JBBs?";
    DefaultEncryptor mEncryptor = null;

    //private MMXClientConfig mConfig = null;

    private MMXMessageListener mMessageListener = new MMXMessageListener() {
        public void onMessageReceiving(MMXMessage message) {
            // TODO Auto-generated method stub

        }

        public void onMessageReceived(final MMXMessage message, String receiptId) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onMessageReceived() start");
            }
            notifyMessageReceived(message, receiptId);
        }

        public void onMessageSending(MMXMessage message, MMXid[] recipients) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onMessageSending() start; msgID=" + message.getId());
            }
        }

        public void onMessageSent(String msgId) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onMessageSent() start; msgID=" + msgId);
            }
        }

        public void onMessageFailed(String msgId) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onMessageFailed() start; msgID=" + msgId);
            }
            notifySendFailed(msgId, null);
        }

        public void onMessageDelivered(MMXid recipient, String msgId) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onMessageDelivered() start");
            }
            notifyMessageDelivered(recipient, msgId);
        }

        public void onInvitationReceived(Invitation invitation) {
            // TODO Auto-generated method stub

        }

        public void onAuthReceived(AuthData auth) {
            // TODO Auto-generated method stub

        }

        public void onItemReceived(MMXMessage message, MMXTopic topic) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onItemReceived() topic=" + topic.getName() + ";message="
                        + message.getPayload().getDataAsText());
            }
            notifyPubsubItemReceived(topic, message);
        }

        public void onErrorMessageReceived(MMXErrorMessage message) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onErrorMessageReceived() msg=" + message);
            }
            notifyErrorReceived(message);

        }
    };

    private MMXClient(String name, Context context, MMXClientConfig config) {
        mName = name;
        mContext = context.getApplicationContext();
        mMMXContext = getMMXContext(mContext);
        mSharedPreferences = getSharedPrefs(context, name);
        mMessagingThread = new HandlerThread("MMXHandlerThread-" + name);
        mMessagingThread.start();
        mMessagingHandler = new Handler(mMessagingThread.getLooper());

        applyConfig(config);
        mConnection = new MMXConnection(mMMXContext, getQueue(), mSettings);
        mConnection.setMessageListener(mMessageListener);

        try {
            MessageDigest digester = MessageDigest.getInstance("SHA-256");
            String encryptionString = mEncryptionString + context.getPackageName() + mMMXContext.getDeviceId();
            digester.update(encryptionString.getBytes());
            mEncryptionKey = digester.digest();
            mEncryptor = new DefaultEncryptor(mEncryptionKey);
        } catch (Exception e) {
            Log.e(TAG, "MMXClient(): Unable to initialize encryptor.", e);
        }
    }

    /**
     * The context associated with this client
     * @return the context
     */
    Context getContext() {
        return mContext;
    }

    MMXConnection getMMXConnection() {
        return mConnection;
    }

    private boolean isApplicationDebuggable(Context context) {
        return ((context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) > 0);
    }

    private MMXContext getMMXContext(Context context) {
        if (isApplicationDebuggable(context)) {
            return new MMXContextImpl(context) {
                @Override
                public String getDeviceId() {
                    String deviceId = mSharedPreferences.getString(SHARED_PREF_KEY_CONFIG_DEBUG_DEVICE_ID, null);
                    Log.d(TAG, "getMMXContext().getDeviceId(): Overridden device id found: " + deviceId);
                    if (deviceId == null) {
                        Log.d(TAG, "getMMXContext().getDeviceId(): Override device id not found, NOT overriding.");
                        return super.getDeviceId();
                    }
                    return deviceId;
                }
            };
        } else {
            return new MMXContextImpl(context);
        }
    }

    /**
     * Set new configuration for this MMXClient instance.  Values will not take effect until the
     * client reconnects.
     *
     * @param newConfig A MMXClientConfig containing the new configuration values
     */
    public synchronized void applyConfig(MMXClientConfig newConfig) {
        boolean isDebugConfig = newConfig instanceof MMXDebugClientConfig;
        if (isDebugConfig && !isApplicationDebuggable(mContext)) {
            throw new IllegalArgumentException(
                    "This application is NOT debuggable but a " + "MMXDebugClientConfig was specified.");
        }
        if (newConfig.getHost() == null || newConfig.getPort() == -1) {
            throw new IllegalArgumentException("The supplied MMXClientConfig does not specify the "
                    + "host or port.  host=" + newConfig.getHost() + ", port=" + newConfig.getPort());
        }
        SharedPreferences.Editor prefEditor = mSharedPreferences.edit();
        prefEditor.putString(SHARED_PREF_KEY_CONFIG_HOST, newConfig.getHost());
        prefEditor.putInt(SHARED_PREF_KEY_CONFIG_PORT, newConfig.getPort());
        prefEditor.putString(SHARED_PREF_KEY_SECURITY_LEVEL, newConfig.getSecurityLevel().name());
        prefEditor.putString(SHARED_PREF_KEY_CONFIG_APP_ID, newConfig.getAppId());
        prefEditor.putString(SHARED_PREF_KEY_CONFIG_API_KEY, newConfig.getApiKey());
        prefEditor.putString(SHARED_PREF_KEY_CONFIG_GCM_PROJECTID, newConfig.getGcmSenderId());
        prefEditor.putString(SHARED_PREF_KEY_CONFIG_SERVER_USER, newConfig.getServerUser());
        prefEditor.putString(SHARED_PREF_KEY_CONFIG_ANONYMOUS_SECRET, newConfig.getAnonymousSecret());
        prefEditor.putString(SHARED_PREF_KEY_CONFIG_DOMAIN_NAME, newConfig.getDomainName());
        if (isDebugConfig) {
            prefEditor.putString(SHARED_PREF_KEY_CONFIG_DEBUG_DEVICE_ID,
                    ((MMXDebugClientConfig) newConfig).getDeviceId());
        } else {
            prefEditor.remove(SHARED_PREF_KEY_CONFIG_DEBUG_DEVICE_ID);
        }
        prefEditor.commit();
        mSettings = buildConnectionSettings(newConfig);
    }

    /**
     * Retrieve the instance of MMXClient with the specified name.
     * If the name doesn't exist, a new client will be created.
     *
     * @param name The name of this instance
     * @param context The context to use for this instance
     * @param config The configuration to use for this instance
     * @return The MMXClient instance
     */
    public static MMXClient getInstance(String name, Context context, MMXClientConfig config) {
        synchronized (sInstanceMap) {
            MMXClient instance = sInstanceMap.get(name);
            if (instance == null) {
                instance = new MMXClient(name, context, config);
                instance.initClient();
                sInstanceMap.put(name, instance);
            } else {
                instance.applyConfig(config);
            }
            return instance;
        }
    }

    /**
     * Retrieve the instance of MMXClient with the default name.  This method
     * will use the specified resId to configure this instance.
     *
     * @param context The context to use for this instance
     * @param configResId The resource id for the configuration properties file (R.raw.xxxxxx)
     * @return The MMXClient instance
     */
    public static MMXClient getInstance(Context context, int configResId) {
        return getInstance(DEFAULT_MMX_NAME, context, configResId);
    }

    /**
     * Retrieve the instance of MMXClient with the specified name.
     * If the name doesn't exist, a new client will be created.  This method
     * will use the specified resId to configure this instance.
     *
     * @param name The name of this instance
     * @param context The context to use for this instance
     * @param configResId The resource id for the configuration properties file (R.raw.xxxxxx)
     * @return The MMXClient instance
     */
    public static MMXClient getInstance(String name, Context context, int configResId) {
        FileBasedClientConfig config = new FileBasedClientConfig(context, configResId);
        return getInstance(name, context, config);
    }

    /**
     * Retrieves the default MMXClient instance.  This is a convenience
     * method for apps that are only working with a single MMXClient.
     *
     * @param context The context to use for this instance
     * @return The MMXClient instance
     */
    public static MMXClient getInstance(Context context, MMXClientConfig config) {
        return getInstance(DEFAULT_MMX_NAME, context, config);
    }

    /**
     * Retrieves an instance of MMXClient that has been previously configured with a MMXClientConfig.
     * Will return null if it hasn't been configured.
     *
     * @param context The context to use for this instance
     * @param name The name of this instance
     * @return The MMXClient instance
     */
    public static MMXClient getExistingInstance(Context context, String name) {
        if (name == null) {
            return null;
        }
        MMXClient instance = null;
        synchronized (sInstanceMap) {
            instance = sInstanceMap.get(name);
            if (instance != null) {
                return instance;
            } else {
                //is there a prefs for this name?
                SharedPreferences prefs = getSharedPrefs(context, name);
                final String appId = prefs.getString(SHARED_PREF_KEY_CONFIG_APP_ID, null);
                if (appId != null) {
                    final String host = prefs.getString(SHARED_PREF_KEY_CONFIG_HOST, null);
                    final int port = prefs.getInt(SHARED_PREF_KEY_CONFIG_PORT, -1);
                    final String securityLevel = prefs.getString(SHARED_PREF_KEY_SECURITY_LEVEL, null);
                    final String apiKey = prefs.getString(SHARED_PREF_KEY_CONFIG_API_KEY, null);
                    final String gcmSenderId = prefs.getString(SHARED_PREF_KEY_CONFIG_GCM_PROJECTID, null);
                    final String serverUser = prefs.getString(SHARED_PREF_KEY_CONFIG_SERVER_USER, null);
                    final String guestPassword = prefs.getString(SHARED_PREF_KEY_CONFIG_ANONYMOUS_SECRET, null);
                    final String domainName = prefs.getString(SHARED_PREF_KEY_CONFIG_DOMAIN_NAME, null);
                    instance = new MMXClient(name, context, new MMXClientConfig() {
                        public String getAppId() {
                            return appId;
                        }

                        public String getApiKey() {
                            return apiKey;
                        }

                        public String getGcmSenderId() {
                            return gcmSenderId;
                        }

                        public String getServerUser() {
                            return serverUser;
                        }

                        public String getAnonymousSecret() {
                            return guestPassword;
                        }

                        public String getHost() {
                            return host;
                        }

                        public int getPort() {
                            return port;
                        }

                        public SecurityLevel getSecurityLevel() {
                            return securityLevel == null ? SecurityLevel.STRICT
                                    : SecurityLevel.valueOf(securityLevel);
                        }

                        public String getDomainName() {
                            return domainName;
                        }
                    });
                    instance.initClient();
                    sInstanceMap.put(name, instance);
                }
                return instance;
            }
        }
    }

    private static SharedPreferences getSharedPrefs(Context context, String name) {
        return context.getSharedPreferences(SHARED_PREF_NAME + name, Context.MODE_PRIVATE);
    }

    /**
     * Get the MMX ID of the current authenticated end-point.
     * @return The MMX ID that represents the current authenticated end-point.
     * @throws MMXException Not connecting to MMX server
     */
    public MMXid getClientId() throws MMXException {
        if (!mConnection.isConnected()) {
            throw new MMXException("Not connecting to MMX server");
        }
        return mConnection.getXID();
    }

    /**
     * Inform the MMX server to suspend delivering messages to this client.
     * @throws MMXException Not connecting to MMX server.
     */
    public void suspendDelivery() throws MMXException {
        if (mConnection == null) {
            throw new MMXException("Not connecting to MMX server");
        }
        mConnection.setMessageFlow(-1);
    }

    /**
     * Inform the MMX server to resume delivering messages to this client.
     * @throws MMXException Not connecting to MMX server.
     */
    public void resumeDelivery() throws MMXException {
        if (mConnection == null) {
            throw new MMXException("Not connecting to MMX server");
        }
        mConnection.setMessageFlow(0);
    }

    /**
     * Helper method to validate username.  This should happen on the server, but
     * validating on the client will prevent an unnecessary network call.
     *
     * @param username the supplied username to validate
     * @return true if valid, false otherwise
     */
    private boolean isValidUsername(String username) {
        if (username == null || username.length() < USERNAME_LENGTH_MIN
                || username.length() > USERNAME_LENGTH_MAX) {
            Log.e(TAG, "isValidUsername(): Username cannot be null and must be in the range " + USERNAME_LENGTH_MIN
                    + "-" + USERNAME_LENGTH_MAX + " inclusive.");
            return false;
        }
        for (int i = USERNAME_INVALID_CHARS.length; --i >= 0;) {
            if (username.indexOf(USERNAME_INVALID_CHARS[i]) != -1) {
                Log.e(TAG,
                        "isValidUsername(): Username cannot contain the characters: " + USERNAME_INVALID_CHARS_STR);
                return false;
            }
        }
        return true;
    }

    private synchronized void connectHelper(final MMXListener listener, final ConnectionOptions options) {
        if (listener == null) {
            //require a listener
            throw new IllegalArgumentException("Listener cannot be null.");
        }
        //register the listener
        mMMXListener = listener;
        mConnectionOptions = options;

        String username = options.getUsername();
        int authMode = (options.isAnonymous() ? MMXConnection.AUTH_ANONYMOUS : 0)
                | (options.isAutoCreate() ? MMXConnection.AUTH_AUTO_CREATE : 0);

        if (!options.isAnonymous() && !isValidUsername(username)) {
            notifyConnectionEvent(ConnectionEvent.CONNECTION_FAILED);
            return;
        }

        //store the info in preferences for now
        SharedPreferences.Editor prefEditor = mSharedPreferences.edit();
        prefEditor.putString(SHARED_PREF_KEY_GCM_REGID, null); //reset the gcm key when doing a new connect.
        prefEditor.putBoolean(SHARED_PREF_KEY_GCM_WAKEUP_ENABLED, true);
        prefEditor.putInt(SHARED_PREF_KEY_AUTH_MODE, authMode);

        prefEditor.commit();
        mConnectionInfo = null; //re-read the connection info

        //if this method is called, it will force a disconnect and reconnect
        disconnect();
        doConnect();
    }

    /**
     * Connect to MMX with the named user and specified password.
     *
     * @param username username
     * @param password password
     * @param listener the listener to use for this connection
     * @param options the connection options to use
     */
    public synchronized void connectWithCredentials(final String username, final byte[] password,
            final MMXListener listener, final ConnectionOptions options) {
        ConnectionOptions cOptions;
        if (options == null) {
            cOptions = new ConnectionOptions();
        } else {
            cOptions = options.clone();
        }
        cOptions.setUsername(username).setPassword(new String(password));
        connectHelper(listener, cOptions);
    }

    /**
     * Connect to MMX without the need to create a username/password.  Subsequent calls using this method
     * will use the same "anonymous" user/password until the data is cleared for this application.
     *
     * @param listener The listener to use for this connection
     * @param options the connection options to use (NOTE: the auto-create option is ignored)
     */
    public synchronized void connectAnonymous(final MMXListener listener, final ConnectionOptions options) {
        // anonymous username and password will be generated.
        ConnectionOptions cOptions;
        if (options == null) {
            cOptions = new ConnectionOptions();
        } else {
            cOptions = options.clone();
        }
        cOptions.setAnonymous(true).setAutoCreate(true);
        connectHelper(listener, cOptions);
    }

    /**
     * If connected as a named user, a complete disconnect will be performed (forgetting user credentials)
     * and the client will subsequently connect anonymously.  If this client is already connected anonymously,
     * this method will do nothing.  If not connected at all, the client will connect anonymously (existing
     * user credentials will be forgotten).
     *
     * Note: if this client was NEVER connected before (no valid connection information), this method will
     * throw an IllegalStateException.
     */
    public void goAnonymous() {
        ConnectionInfo info = getConnectionInfo();
        if (mMMXListener == null) {
            throw new IllegalArgumentException("Cannot call goAnonymous() without first calling connect()");
        }
        if (isConnected()) {
            if ((info.authMode & MMXConnection.AUTH_ANONYMOUS) != 0) {
                //if already anonymous, nothing to do
                return;
            } else {
                //connected but not anonymous, disconnect completely first.
                disconnect(true);
                //There's a slight chance of a race condition here if the other thread executes
                //the disconnect so fast that mIsDisconnecting is notified before the wait.
                synchronized (mIsDisconnecting) {
                    try {
                        mIsDisconnecting.wait();
                    } catch (InterruptedException e) {
                        Log.e(TAG, "goAnonymous(): caught exception while waiting for disconnect.");
                    }
                }
            }
        }
        //finally connect anonymously
        connectAnonymous(mMMXListener, null);
    }

    /**
     * Retrieves the current connection info.
     * @return
     */
    public synchronized ConnectionInfo getConnectionInfo() {
        if (mConnectionInfo == null) {
            String gcmRegId = mSharedPreferences.getString(SHARED_PREF_KEY_GCM_REGID, null);
            int gcmRegIdAppVersion = mSharedPreferences.getInt(SHARED_PREF_KEY_GCM_REGID_APPVERSION, -1);
            boolean isWakeupEnabled = mSharedPreferences.getBoolean(SHARED_PREF_KEY_GCM_WAKEUP_ENABLED, true);
            int authMode = mSharedPreferences.getInt(SHARED_PREF_KEY_AUTH_MODE, 0);
            String securityLevelStr = mSharedPreferences.getString(SHARED_PREF_KEY_SECURITY_LEVEL,
                    SecurityLevel.STRICT.name());

            //build the client config
            final String appId = mSharedPreferences.getString(SHARED_PREF_KEY_CONFIG_APP_ID, null);
            final String apiKey = mSharedPreferences.getString(SHARED_PREF_KEY_CONFIG_API_KEY, null);
            final String gcmSenderId = mSharedPreferences.getString(SHARED_PREF_KEY_CONFIG_GCM_PROJECTID, null);
            final String serverUser = mSharedPreferences.getString(SHARED_PREF_KEY_CONFIG_SERVER_USER, null);
            final String anonymousSecret = mSharedPreferences.getString(SHARED_PREF_KEY_CONFIG_ANONYMOUS_SECRET,
                    null);
            final String host = mSharedPreferences.getString(SHARED_PREF_KEY_CONFIG_HOST, null);
            final int port = mSharedPreferences.getInt(SHARED_PREF_KEY_CONFIG_PORT, -1);
            final SecurityLevel securityLevel = SecurityLevel.valueOf(securityLevelStr);
            final String domainName = mSharedPreferences.getString(SHARED_PREF_KEY_CONFIG_DOMAIN_NAME, null);

            MMXClientConfig config = new MMXClientConfig() {
                public String getAppId() {
                    return appId;
                }

                public String getApiKey() {
                    return apiKey;
                }

                public String getGcmSenderId() {
                    return gcmSenderId;
                }

                public String getServerUser() {
                    return serverUser;
                }

                public String getAnonymousSecret() {
                    return anonymousSecret;
                }

                public String getHost() {
                    return host;
                }

                public int getPort() {
                    return port;
                }

                public SecurityLevel getSecurityLevel() {
                    return securityLevel;
                }

                public String getDomainName() {
                    return domainName;
                }
            };
            mConnectionInfo = new ConnectionInfo(config,
                    mConnectionOptions != null ? mConnectionOptions.getUsername() : null,
                    mConnectionOptions != null ? mConnectionOptions.getPassword() : null, gcmRegId,
                    gcmRegIdAppVersion, isWakeupEnabled, authMode);
        }
        return mConnectionInfo;
    }

    private final Runnable mConnectionRunnable = new Runnable() {
        public void run() {
            try {
                if (mIsDisconnecting.get()) {
                    synchronized (mIsDisconnecting) {
                        mIsDisconnecting.wait();
                    }
                }
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "ConnectionRunnable: begin");
                }
                if (mConnection != null && mConnection.isConnected()) {
                    if (Log.isLoggable(TAG, Log.DEBUG)) {
                        Log.d(TAG, "ConnectionRunnable: already connected, returning.");
                    }
                    return;
                }
                ConnectionInfo connectionInfo = getConnectionInfo();
                MMXClientConfig config = connectionInfo.clientConfig;

                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "ConnectionRunnable: attempting connection to: " + config.getHost() + " on port "
                            + config.getPort());
                }
                SecurityLevel securityLevel = config.getSecurityLevel();
                HostnameVerifier verifier = null;
                SSLContext sslContext = null;
                if (securityLevel == SecurityLevel.RELAXED) {
                    //if security is "RELAXED"
                    //sslContext is used when the socket is being upgraded during starttls
                    sslContext = getNaiveSSLContext();
                    verifier = getNaiveHostnameVerifier();
                }
                mConnection.connect(new MMXConnectionListener(), verifier,
                        new MMXSocketFactoryWrapper(SocketFactory.getDefault()), sslContext,
                        mConnectionOptions.isSuspendDelivery());

                if ((connectionInfo.authMode & MMXConnection.AUTH_ANONYMOUS) != 0) {
                    mConnection.loginAnonymously();
                } else {
                    if (connectionInfo.username == null) {
                        //anonymous has a generated username/password
                        throw new IllegalArgumentException("Unable to login with null username");
                    } else {
                        String username = connectionInfo.username;
                        String deviceId = mMMXContext.getDeviceId();
                        if (Log.isLoggable(TAG, Log.DEBUG)) {
                            Log.d(TAG, "ConnectionRunnable: Attempting login with " + username + ", resource="
                                    + deviceId + ", authMode=" + connectionInfo.authMode);
                        }
                        mConnection.authenticate(username, connectionInfo.password, deviceId,
                                connectionInfo.authMode);
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "ConnectionRunnable:  Connection failed.  Exception caught.", e);
                disconnect();
                notifyConnectionEvent(ConnectionEvent.CONNECTION_FAILED);
            }
        }
    };

    private void doConnect() {
        synchronized (mMessagingHandler) {
            mMessagingHandler.post(mConnectionRunnable);
        }
    }

    private MMXSettings buildConnectionSettings(MMXClientConfig config) {
        MMXSettings settings = new MMXSettingsImpl();
        settings.setString(MMXSettings.PROP_HOST, config.getHost());
        settings.setInt(MMXSettings.PROP_PORT, config.getPort());
        settings.setString(MMXSettings.PROP_APPID, config.getAppId());
        settings.setString(MMXSettings.PROP_APIKEY, config.getApiKey());
        settings.setString(MMXSettings.PROP_SERVERUSER, config.getServerUser());
        settings.setString(MMXSettings.PROP_GUESTSECRET, config.getAnonymousSecret());
        settings.setString(MMXSettings.PROP_SERVICE_NAME, config.getDomainName());
        SecurityLevel securityLevel = config.getSecurityLevel();
        settings.setBoolean(MMXSettings.PROP_ENABLE_TLS, securityLevel != SecurityLevel.NONE);
        return settings;
    }

    final AtomicBoolean mIsDisconnecting = new AtomicBoolean(false);

    /**
     * Disconnect the client completely from MMX.
     * This method will deactivate the device if the flag is specified.
     *
     * @param deactivateDevice true indicates that the device should be deactivated
     */
    public void disconnect(final boolean deactivateDevice) {
        synchronized (mMessagingHandler) {
            if (mIsDisconnecting.get()) {
                return;
            }
            mIsDisconnecting.set(true);
            mMessagingHandler.post(new Runnable() {
                public void run() {
                    if (deactivateDevice && isConnected()) {
                        if (!isConnected()) {
                            Log.e(TAG,
                                    "disconnect(): not connected, cannot deactivate the device. connect and try again.");
                            return;
                        }
                        //unregister device
                        try {
                            if (Log.isLoggable(TAG, Log.DEBUG)) {
                                Log.d(TAG, "disconnect():  deactivating device");
                            }
                            MMXDeviceManager deviceManager = getDeviceManager();
                            MMXStatus status = DeviceManager.getInstance(mConnection)
                                    .unregister(mMMXContext.getDeviceId());
                            if (Log.isLoggable(TAG, Log.DEBUG)) {
                                Log.d(TAG, "disconnect(): deactivation completed with status=" + status);
                            }
                        } catch (MMXException e) {
                            Log.e(TAG, "disconnect(): caught exception while deactivating", e);
                        }

                        //clear settings
                        synchronized (MMXClient.this) {
                            if (Log.isLoggable(TAG, Log.DEBUG)) {
                                Log.d(TAG, "disconnect():  clearing stored connection information");
                            }
                            mSharedPreferences.edit().remove(SHARED_PREF_KEY_GCM_REGID)
                                    .remove(SHARED_PREF_KEY_GCM_REGID_APPVERSION).remove(SHARED_PREF_KEY_AUTH_MODE)
                                    .remove(SHARED_PREF_KEY_GCM_WAKEUP_ENABLED).commit();
                            mConnectionInfo = null;
                            mConnectionOptions = null;

                            //clear the database and filesystem
                            if (Log.isLoggable(TAG, Log.DEBUG)) {
                                Log.d(TAG, "disconnect(): removing database and pending send messages");
                            }
                            getQueue().removeAllItems();
                        }
                    }

                    //finally, disconnect
                    if (Log.isLoggable(TAG, Log.DEBUG)) {
                        Log.d(TAG, "disconnect():  disconnecting from server");
                    }
                    if (!isConnected()) {
                        if (Log.isLoggable(TAG, Log.DEBUG)) {
                            Log.d(TAG, "disconnect():  not connected, so just notifying the callback.");
                        }
                        synchronized (mIsDisconnecting) {
                            mIsDisconnecting.set(false);
                            mIsDisconnecting.notify();
                        }
                    } else {
                        mConnection.disconnect();
                    }
                    mConnection.destroy();
                    mConnection = new MMXConnection(mMMXContext, getQueue(), mSettings);
                    mConnection.setMessageListener(mMessageListener);

                    synchronized (MMXClient.this) {
                        //notify the internal managers that the connection has changed
                        for (MMXManager manager : mManagers.values()) {
                            manager.onConnectionChanged();
                        }
                    }
                }
            });
        }
    }

    synchronized PersistentQueue getQueue() {
        if (mQueue == null) {
            mQueue = new PersistentQueue(this);
        }
        return mQueue;
    }

    /**
     * Disconnects from the server, but the device/user remain
     * registered and a wakeup will connect the user with the arguments
     * that were previously supplied in the connect() method.
     */
    public void disconnect() {
        disconnect(false);
    }

    /**
     * If the MMXClient is currently connected.
     * @return true if the client is currently connected
     */
    public boolean isConnected() {
        return mConnection != null && mConnection.isConnected();
    }

    @SuppressWarnings("unchecked")
    static void handleWakeup(Context context, Intent intent) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "handleWakeup(): starting.");
        }
        synchronized (MMXClient.class) {
            SharedPreferences prefs = context.getSharedPreferences(STATIC_SHARED_PREF_NAME, Context.MODE_PRIVATE);
            String wakeupListenerClassname = prefs.getString(STATIC_SHARED_PREF_KEY_WAKEUP_LISTENER_CLASS, null);
            if (wakeupListenerClassname == null) {
                Log.e(TAG, "handleWakeup(): THERE IS NO WAKEUP LISTENER SPECIFIED.  "
                        + "The application should specify this by calling MMXClient.registerWakeupListener().");
            } else {
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "handleWakeup():  Looking up wakeup listener: " + wakeupListenerClassname);
                }
                try {
                    Class<? extends MMXWakeupListener> clazz = (Class<? extends MMXWakeupListener>) Class
                            .forName(wakeupListenerClassname);
                    MMXWakeupListener wakeupListener = clazz.newInstance();
                    wakeupListener.onWakeupReceived(context.getApplicationContext(), intent);
                } catch (Exception e) {
                    Log.e(TAG, "handleWakeup(): Exception caught while calling the wakeup listener: "
                            + wakeupListenerClassname);
                }
            }
            //TODO:  perhaps only do this if the listener was called successfully.
            scheduleWakeupAlarm(context, getWakeupInterval(context));
        }
    }

    private static void scheduleWakeupAlarm(Context context, long interval) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "scheduleWakeupAlarm(): called with interval=" + interval);
        }
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        if (interval > 0) {
            //a time was set
            alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + interval,
                    getWakeupIntent(context));
        } else {
            //cancel
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "scheduleWakeupAlarm(): cancelling alarm");
            }
            alarmManager.cancel(getWakeupIntent(context));
        }
    }

    static PendingIntent getWakeupIntent(Context context) {
        Intent intent = new Intent(context, MMXWakeupIntentService.class);
        intent.setAction(ACTION_WAKEUP);
        return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
    }

    static long getWakeupInterval(Context context) {
        SharedPreferences prefs = context.getSharedPreferences(STATIC_SHARED_PREF_NAME, Context.MODE_PRIVATE);
        return prefs.getLong(STATIC_SHARED_PREF_KEY_WAKEUP_INTERVAL, 0l);
    }

    /**
     * This will set the polling interval for messages in milliseconds and then
     * schedule the next alarm.  The alarm will be a WAKEUP alarm and cause the device
     * to stay awake, so be cautious when setting to small intervals.
     *
     * The Application.onCreate should specify a wakeup listener class MMXContext.registerWakeupListener()
     *
     * @param context The Android context
     * @param interval The amount of time to wait between wakeup alarms.  0 will cancel the wakeup
     */
    public static void setWakeupInterval(Context context, long interval) {
        SharedPreferences prefs = context.getSharedPreferences(STATIC_SHARED_PREF_NAME, Context.MODE_PRIVATE);
        prefs.edit().putLong(STATIC_SHARED_PREF_KEY_WAKEUP_INTERVAL, interval).commit();
        scheduleWakeupAlarm(context, interval);
    }

    /**
     * Register the wakeup listener for MMX.  There can only be one registered
     * wakeup listener class.
     *
     * @param context The Android context
     * @param wakeupListenerClass The class of that implements the MMXWakeupListener interface
     */
    public static void registerWakeupListener(Context context,
            Class<? extends MMXWakeupListener> wakeupListenerClass) {
        synchronized (MMXClient.class) {
            SharedPreferences prefs = context.getSharedPreferences(STATIC_SHARED_PREF_NAME, Context.MODE_PRIVATE);
            prefs.edit().putString(STATIC_SHARED_PREF_KEY_WAKEUP_LISTENER_CLASS,
                    wakeupListenerClass != null ? wakeupListenerClass.getName() : null).commit();
        }
    }

    private void notifyConnectionEvent(final ConnectionEvent event) {
        synchronized (this) {
            mMessagingHandler.post(new Runnable() {
                public void run() {
                    try {
                        mMMXListener.onConnectionEvent(MMXClient.this, event);
                    } catch (Exception ex) {
                        Log.e(TAG, "notifyConnectionEvent(): Caught runtime exception during " + "the callback",
                                ex);
                    }
                }
            });
        }
    }

    private void notifyErrorReceived(final MMXErrorMessage message) {
        synchronized (this) {
            mMessagingHandler.post(new Runnable() {
                public void run() {
                    try {
                        mMMXListener.onErrorReceived(MMXClient.this, message);
                    } catch (Exception ex) {
                        Log.e(TAG, "notifyConnectionEvent(): Caught runtime exception during " + "the callback",
                                ex);
                    }
                }
            });
        }
    }

    private void notifyMessageReceived(final MMXMessage message, final String receiptId) {
        synchronized (this) {
            mMessagingHandler.post(new Runnable() {
                public void run() {
                    try {
                        mMMXListener.onMessageReceived(MMXClient.this, message, receiptId);
                    } catch (Exception ex) {
                        Log.e(TAG, "notifyConnectionEvent(): Caught runtime exception during " + "the callback",
                                ex);
                    }
                }
            });
        }
    }

    private void notifyPubsubItemReceived(final MMXTopic topic, final MMXMessage message) {
        synchronized (this) {
            mMessagingHandler.post(new Runnable() {
                public void run() {
                    try {
                        mMMXListener.onPubsubItemReceived(MMXClient.this, topic, message);
                    } catch (Exception ex) {
                        Log.e(TAG, "notifyConnectionEvent(): Caught runtime exception during " + "the callback",
                                ex);
                    }
                }
            });
        }
    }

    private void notifyMessageDelivered(final MMXid recipient, final String messageId) {
        synchronized (this) {
            mMessagingHandler.post(new Runnable() {
                public void run() {
                    try {
                        mMMXListener.onMessageDelivered(MMXClient.this, recipient, messageId);
                    } catch (Exception ex) {
                        Log.e(TAG, "notifyConnectionEvent(): Caught runtime exception during " + "the callback",
                                ex);
                    }
                }
            });
        }
    }

    private void notifySendFailed(final String messageId, final MMXException cause) {
        synchronized (this) {
            mMessagingHandler.post(new Runnable() {
                public void run() {
                    try {
                        mMMXListener.onSendFailed(MMXClient.this, messageId);
                    } catch (Exception ex) {
                        Log.e(TAG, "notifySendFailed(): Caught runtime exception during " + "the callback", ex);
                    }
                }
            });
        }
    }

    /**
     * Register with the gcm service.  This will retrieve the gcm token if it doesn't already
     * exist.
     */
    private void initClient() {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "initClient(): starting.");
        }
        SmackAndroid.init(mContext);
    }

    private void registerDeviceWithServer() {
        synchronized (mMessagingHandler) {
            mMessagingHandler.post(new Runnable() {
                public void run() {
                    if (Log.isLoggable(TAG, Log.DEBUG)) {
                        Log.d(TAG, "registerDeviceWithServer() start");
                    }
                    try {
                        ConnectionInfo connectionInfo = getConnectionInfo();
                        int playServicesResult = GooglePlayServicesUtil.isGooglePlayServicesAvailable(mContext);
                        if (playServicesResult == ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED) {
                            GooglePlayServicesUtil.showErrorNotification(playServicesResult, mContext);
                        }
                        String gcmSenderId = connectionInfo.clientConfig.getGcmSenderId();
                        boolean isGcmWakeupEnabled = connectionInfo.isGcmWakeupEnabled
                                && ConnectionResult.SUCCESS == playServicesResult && gcmSenderId != null
                                && !gcmSenderId.trim().isEmpty();
                        if (isGcmWakeupEnabled) {
                            GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(mContext);
                            String gcmRegId = connectionInfo.gcmRegId;
                            int gcmRegIdAppVersion = connectionInfo.gcmRegIdAppVersion;
                            int appVersion = getAppVersion();
                            boolean isNeedNewToken = gcmRegId == null || gcmRegIdAppVersion < 0
                                    || gcmRegIdAppVersion != appVersion;

                            if (isNeedNewToken) {
                                if (Log.isLoggable(TAG, Log.DEBUG)) {
                                    Log.d(TAG, "registerDeviceWithServer() need new gcm token, registering.");
                                }
                                try {
                                    gcmRegId = gcm.register(gcmSenderId);
                                    synchronized (MMXClient.this) {
                                        SharedPreferences.Editor prefEditor = mSharedPreferences.edit();
                                        prefEditor.putString(SHARED_PREF_KEY_GCM_REGID, gcmRegId);
                                        prefEditor.putInt(SHARED_PREF_KEY_GCM_REGID_APPVERSION, appVersion);
                                        prefEditor.commit();
                                        mConnectionInfo = null;

                                        //reload the connection info since been updated
                                        connectionInfo = getConnectionInfo();
                                    }
                                } catch (Exception ex) {
                                    Log.e(TAG, "registerDeviceWithServer() GCM registration failed!", ex);
                                    notifyConnectionEvent(ConnectionEvent.WAKEUP_REGISTRATION_FAILED);
                                }
                            }
                        }

                        //Register this device with MMX server.
                        if (Log.isLoggable(TAG, Log.DEBUG)) {
                            Log.d(TAG, "registerDeviceWithServer() create DevReg.Request()");
                        }
                        DevReg devReg = new DevReg();
                        devReg.setPushType(isGcmWakeupEnabled ? PushType.GCM.toString() : null);
                        devReg.setPushToken(isGcmWakeupEnabled ? connectionInfo.gcmRegId : null);
                        devReg.setDevId(mMMXContext.getDeviceId());
                        devReg.setModelInfo(Build.MANUFACTURER + " " + Build.MODEL);
                        // TODO: it will be nice to get the Security Setting's Owner info
                        devReg.setDisplayName(devReg.getModelInfo());
                        try {
                            CarrierEnum carrier = DeviceUtil.getCarrier(mContext);
                            devReg.setCarrierInfo((carrier == null) ? null : carrier.toString());
                        } catch (SecurityException ex) {
                            Log.w(TAG,
                                    "registerDeviceWithServer(): Unable to get carrier info: " + ex.getMessage());
                        }
                        try {
                            devReg.setPhoneNumber(DeviceUtil.getLineNumber(mContext));
                        } catch (SecurityException ex) {
                            Log.w(TAG, "registerDeviceWithServer(): Unable to get phone number info: "
                                    + ex.getMessage());
                        }
                        devReg.setOsType(OSType.ANDROID.toString());
                        devReg.setOsVersion(Build.VERSION.RELEASE);
                        // Register the client protocol version numbers.
                        devReg.setVersionMajor(Constants.MMX_VERSION_MAJOR);
                        devReg.setVersionMinor(Constants.MMX_VERSION_MINOR);
                        MMXStatus status = getDeviceManager().register(devReg);
                        if (Log.isLoggable(TAG, Log.DEBUG)) {
                            Log.d(TAG, "registerDeviceWithServer(): device registration completed with status="
                                    + status);
                        }
                        notifyConnectionEvent(ConnectionEvent.CONNECTED);
                        getQueue().processPendingItems();
                    } catch (MMXException e) {
                        Log.e(TAG, "registerDeviceWithServer(): caught MMXException code=" + e.getCode(), e);
                        if (e.getCode() == 400) {
                            //if status is unsuccessful, disconnect
                            notifyConnectionEvent(ConnectionEvent.AUTHENTICATION_FAILURE);
                            disconnect();
                        }
                    }
                }
            });
        }
    }

    /**
     * Retrieves the app version number from PackageManager.
     * @return
     */
    private int getAppVersion() {
        try {
            PackageInfo pi = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
            return pi.versionCode;
        } catch (NameNotFoundException e) {
            Log.e(TAG, "getAppVersion(): exception caught ", e);
        }
        return -1;
    }

    /**
     * The MMX connection listener that listens for Smack connection events.
     * @author login7
     */
    private class MMXConnectionListener implements com.magnet.mmx.client.common.MMXConnectionListener {
        private final String TAG = MMXConnectionListener.class.getSimpleName();

        public void onAuthenticated(String user) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onAuthenticated() begin");
            }
            registerDeviceWithServer();
        }

        @Override
        public void onAuthFailed(String user) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onAuthFailed() begin");
            }
            notifyConnectionEvent(ConnectionEvent.AUTHENTICATION_FAILURE);
            disconnect();
        }

        public void onConnectionEstablished() {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onConnectionEstablished() begin");
            }
            //notifyConnectionEvent(ConnectionEvent.CONNECTED);
        }

        public void onConnectionFailed(Exception cause) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onConnectionFailed() begin");
            }
            notifyConnectionEvent(ConnectionEvent.CONNECTION_FAILED);
        }

        public void onConnectionClosed() {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onConnectionClosed() begin");
            }
            notifyConnectionEvent(ConnectionEvent.DISCONNECTED);
            synchronized (mIsDisconnecting) {
                mIsDisconnecting.set(false);
                mIsDisconnecting.notify();
            }
        }

        @Override
        public void onAccountCreated(String user) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onAccountCreated() user=" + user);
            }
        }

    }

    /**
     * Implement if interested in MMXClient events.  In most cases,
     * this should be implemented and registered with a subsequent call
     * to registerMMXListener.
     */
    public interface MMXListener {
        /**
         * Called when a connection event occurs.
         * @param client The instance of the MMXClient
         * @param event The event that occurred
         */
        public void onConnectionEvent(MMXClient client, ConnectionEvent event);

        /**
         * Called when a message is received.
         *
         * @param client The instance of the MMXClient
         * @param message The message that was received
         * @param receiptId A delivery receipt ID or null.
         */
        public void onMessageReceived(MMXClient client, MMXMessage message, String receiptId);

        /**
         * Called when a message send fails due to the loss of connection.
         * @param client The instance of the MMXClient
         * @param messageId The id of the message that failed
         */
        public void onSendFailed(MMXClient client, String messageId);

        /**
         * Called when a delivery is confirmed for a message that was sent
         * with the ack option.
         *
         * @param client The instance of the MMXClient
         * @param messageId The id of the message for which the receipt was returned
         */
        public void onMessageDelivered(MMXClient client, MMXid recipient, String messageId);

        /**
         * Called when a pubsub item is received.
         * @param client The instance of the MMXClient
         * @param topic The topic for the pubsub item that was received
         * @param message The message of the pubsub item
         */
        public void onPubsubItemReceived(MMXClient client, MMXTopic topic, MMXMessage message);

        /**
         * Called when an error message is received.  The payload in the error
         * message can be an MMXError or custom; use {@link MMXError#getType()} to identify the payload, and use
         * {@link MMXError#fromJson(String)} and {@link MMXPayload#getDataAsText()}
         * to construct the MMXError payload.
         * @param client The instance of the MMXClient
         * @param error The error message
         */
        public void onErrorReceived(MMXClient client, MMXErrorMessage error);
    }

    /**
     * The current connection properties for this instance of MMXClient.  This contains the latest
     * persisted values that will take effect on the next connect() call.  The caller should not
     * cache this object, but instead call getConnectionInfo() to get the most up-to-date values.
     */
    public final class ConnectionInfo {
        /**
         * The MMXClientConfig that is used for this instance
         */
        public final MMXClientConfig clientConfig;
        /**
         * The username that this MMXClient is connected with
         */
        public final String username;
        private final String password;
        /**
         * The GCM registration id if this device has been registered
         */
        public final String gcmRegId;
        /**
         * The GCM registration app version if this device has been registered
         */
        public final int gcmRegIdAppVersion;
        /**
         * Whether GCM wakeup is currently enabled
         */
        public final boolean isGcmWakeupEnabled;
        /**
         * The auth mode for this MMXClient instance
         */
        public final int authMode;

        private ConnectionInfo(MMXClientConfig config, String username, String password, String gcmRegId,
                int gcmRegIdAppVersion, boolean isGcmWakeupEnabled, int authMode) {
            this.clientConfig = config;
            this.username = username;
            this.password = password;
            this.gcmRegId = gcmRegId;
            this.gcmRegIdAppVersion = gcmRegIdAppVersion;
            this.isGcmWakeupEnabled = isGcmWakeupEnabled;
            this.authMode = authMode;
        }
    }

    /**
     * Returns whether or not the wake-up functionality is enabled
     * for this MMX Client.  Wake-up functionality allows the MMX server
     * to wake up the device to deliver messages to the MMX client application.
     *
     * Wake-up functionality is only supported after the application invokes the
     * connect method successfully.
     *
     * @return true if GCM wakeup is enabled for this client
     */
    public boolean isGcmWakeUpEnabled() {
        return getConnectionInfo().isGcmWakeupEnabled;
    }

    /**
     * Enables/disables the wake-up functionality for this MMX client.  See
     * isWakeUpEnabled() for more details.
     *
     * @param isWakeupEnabled true to enable GCM push messages
     */
    public void setGcmWakeUpEnabled(boolean isWakeupEnabled) {
        synchronized (MMXClient.this) {
            SharedPreferences.Editor prefEditor = mSharedPreferences.edit();
            prefEditor.putBoolean(SHARED_PREF_KEY_GCM_WAKEUP_ENABLED, isWakeupEnabled);
            prefEditor.commit();
            mConnectionInfo = null;
        }
        registerDeviceWithServer();
    }

    /**
     * Implement this interface with a default public constructor.  This implementation is required
     * if the application needs to handle any wake-up events (including GCM push messages or timer-based
     * wake-ups).
     *
     * To use the MMX client wake-up functionality, register the MMXWakeupListener implementation
     * {@link #registerWakeupListener(Context, Class)}.  Setup either a scheduled wake-up
     * {@link #scheduleWakeupAlarm(Context, long)} OR configure GCM for your application
     * {@see https://developers.google.com/cloud-messaging/android/start} and register the
     * GCM project ID in the MMX client configuration {@link MMXClientConfig}.
     */
    public interface MMXWakeupListener {
        /**
         * Called when the application has been woken up either from
         * a GCM or some other MMX wakeup mechanism.
         *
         * @param applicationContext The application's context
         * @param intent The intent that caused this wakeup
         */
        void onWakeupReceived(Context applicationContext, Intent intent);
    }

    private synchronized HostnameVerifier getNaiveHostnameVerifier() {
        if (mNaiveHostnameVerifier == null) {
            mNaiveHostnameVerifier = new AllowAllHostnameVerifier();
        }
        return mNaiveHostnameVerifier;
    }

    /**
     * Doesn't throw exceptions when any SSL cert is handed to it.
     * Allows all certs.
     */
    private static class NaiveTrustManager implements X509TrustManager {
        public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
            Log.d(TAG, "NaiveTrustManager.checkClientTrusted() start");
        }

        public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
            Log.d(TAG, "NaiveTrustManager.checkServerTrusted() start");
        }

        public X509Certificate[] getAcceptedIssuers() {
            Log.d(TAG, "NaiveTrustManager.getAcceptedIssuers() start");
            return null;
        }

    }

    private SSLContext mNaiveSslContext = null;
    private HostnameVerifier mNaiveHostnameVerifier = null;

    private synchronized SSLContext getNaiveSSLContext() {
        if (mNaiveSslContext == null) {
            try {
                TrustManager[] tm = new TrustManager[] { new NaiveTrustManager() };
                mNaiveSslContext = SSLContext.getInstance("TLS");
                mNaiveSslContext.init(null, tm, new SecureRandom());
            } catch (Exception e) {
                Log.e(TAG, "getNaiveSSLContext(): caught exception", e);
            }
        }
        return mNaiveSslContext;
    }

    class MMXSocketFactoryWrapper extends SocketFactory {
        private SocketFactory mBaseFactory;

        public MMXSocketFactoryWrapper(SocketFactory baseFactory) {
            mBaseFactory = baseFactory;
        }

        /**
         * If SSL is required, the caller MUST verify the server's
         * identify when calling this method by using a HostnameVerifier.verify().
         */
        public Socket createSocket(String s, int i) throws IOException, UnknownHostException {
            Socket socket = mBaseFactory.createSocket();
            if (socket instanceof SSLSocket) {
                SSLSocket sslSocket = (SSLSocket) socket;
                sslSocket.setEnabledCipherSuites(sslSocket.getSupportedCipherSuites());
            }
            InetSocketAddress addr = new InetSocketAddress(s, i);
            socket.connect(addr, TCP_CONNECTION_TIMEOUT);
            return socket;
        }

        @Override
        public Socket createSocket(String s, int i, InetAddress inetAddress, int i2)
                throws IOException, UnknownHostException {
            return mBaseFactory.createSocket(s, i, inetAddress, i2);
        }

        @Override
        public Socket createSocket(InetAddress inetAddress, int i) throws IOException {
            return mBaseFactory.createSocket(inetAddress, i);
        }

        @Override
        public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress2, int i2)
                throws IOException {
            return mBaseFactory.createSocket(inetAddress, i, inetAddress2, i2);
        }
    }

    /**
     * Publishes the current location to the user's built-in topic.
     * Uses play services to determine location.  The application must declare
     * the location permissions in order to use this method (these are automatically added
     * when using the gradle build).
     *
     * <code><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/></code>
     * <code><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/></code>
     *
     * Note:  This method is not blocking and will execute the publish location request when available.
     * The location is timestamped at the time of the publishLocation() call.
     *
     * @return false if Google Play Services is unavailable.  True if the publish call is submitted successfully
     */
    public boolean updateLocation() {
        if (playServicesConnected()) {
            mMessagingHandler.post(new Runnable() {
                public void run() {
                    MMXPlayServicesCallback callback = new MMXPlayServicesCallback();
                    GoogleApiClient googleApiClient = new GoogleApiClient.Builder(mContext)
                            .addApi(LocationServices.API).addConnectionCallbacks(callback)
                            .addOnConnectionFailedListener(callback).build();
                    googleApiClient.connect();
                    synchronized (callback) {
                        try {
                            callback.wait(5000);
                            if (callback.mIsConnected) {
                                Location currentLocation = LocationServices.FusedLocationApi
                                        .getLastLocation(googleApiClient);
                                if (currentLocation == null) {
                                    Log.e(TAG,
                                            "publishLocation(): Unable to retrieve location from locationClient.  "
                                                    + "Ensure that the proper permissions(android.permission.ACCESS_COARSE_LOCATION, "
                                                    + "android.permission.ACCESS_FINE_LOCATION) have been declared in the "
                                                    + "AndroidManifest.xml file.  Skipping...");
                                } else {
                                    Date locationDate = new Date(currentLocation.getTime());
                                    if (Log.isLoggable(TAG, Log.DEBUG)) {
                                        Log.d(TAG,
                                                "publishLocation(): location " + "  lat="
                                                        + currentLocation.getLatitude() + ", long="
                                                        + currentLocation.getLongitude() + ", accuracy="
                                                        + currentLocation.getAccuracy() + ", provider="
                                                        + currentLocation.getProvider() + ", time=" + locationDate);
                                    }
                                    GeoLoc geo = new GeoLoc();
                                    geo.setAccuracy((int) currentLocation.getAccuracy());
                                    geo.setLat((float) currentLocation.getLatitude());
                                    geo.setLng((float) currentLocation.getLongitude());
                                    String publishedId = MMXGeoLogger.updateGeoLocation(MMXClient.this, geo);
                                    if (Log.isLoggable(TAG, Log.DEBUG)) {
                                        Log.d(TAG, "publishLocation(): completed.  id=" + publishedId);
                                    }
                                }
                            } else {
                                Log.w(TAG, "publishLocation(): unable to connection location client");
                            }
                        } catch (InterruptedException e) {
                            Log.e(TAG, "publishLocation(): caught exception waiting for location client", e);
                        } catch (MMXException e) {
                            Log.e(TAG, "publishLocation(): caught exception while publishing location", e);
                        } finally {
                            googleApiClient.disconnect();
                        }
                    }
                }
            });
            return true;
        } else {
            Log.e(TAG, "publishLocation(): Unable to publish location because play services is not available.");
            return false;
        }
    }

    private boolean playServicesConnected() {
        // Check that Google Play services is available
        int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(mContext);
        // If Google Play services is available
        if (ConnectionResult.SUCCESS == resultCode) {
            // In debug mode, log the status
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "playServicesConnected():  Google Play services is available.");
            }
            // Continue
            return true;
            // Google Play services was not available for some reason.
            // resultCode holds the error code.
        } else {
            // log an error
            Log.e(TAG, "playServicesConnected(): Google Play services is NOT AVAILABLE.");
            return false;
        }
    }

    private static class MMXPlayServicesCallback
            implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
        private static final String TAG = MMXPlayServicesCallback.class.getSimpleName();
        private boolean mIsConnected = false;

        public void onConnected(Bundle bundle) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onConnected(): start");
            }
            synchronized (this) {
                mIsConnected = true;
                this.notify();
            }
        }

        public void onConnectionSuspended(int i) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onConnectionSuspended(): start");
            }
            synchronized (this) {
                mIsConnected = false;
                this.notify();
            }
        }

        public void onConnectionFailed(ConnectionResult connectionResult) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onConnectionFailed(): start");
            }
            synchronized (this) {
                mIsConnected = false;
                this.notify();
            }
        }
    }

    /**
     * Retrieve the MMXMessageManager instance for this client.
     *
     * @return the MMXMessageManager instance associated with this client
     */
    public synchronized MMXMessageManager getMessageManager() {
        MMXManager manager = mManagers.get(MMXMessageManager.class);
        if (manager == null) {
            manager = new MMXMessageManager(this, mMessagingHandler);
            mManagers.put(MMXMessageManager.class, manager);
        }
        return (MMXMessageManager) manager;
    }

    /**
     * Retrieve the MMXPubSubManager instance for this client.
     *
     * @return the MMXPubSubManager instance associated with this client
     */
    public synchronized MMXPubSubManager getPubSubManager() {
        MMXManager manager = mManagers.get(MMXPubSubManager.class);
        if (manager == null) {
            manager = new MMXPubSubManager(this, mMessagingHandler);
            mManagers.put(MMXPubSubManager.class, manager);
        }
        return (MMXPubSubManager) manager;
    }

    /**
     * Retrieve the MMXAccountManager instance for this client.
     *
     * @return the MMXAccountManager instance associated with this client
     */
    public synchronized MMXAccountManager getAccountManager() {
        MMXManager manager = mManagers.get(MMXAccountManager.class);
        if (manager == null) {
            manager = new MMXAccountManager(this, mMessagingHandler);
            mManagers.put(MMXAccountManager.class, manager);
        }
        return (MMXAccountManager) manager;
    }

    /**
     * Retrieve the MMXDeviceManager instance for this client.
     *
     * @return the MMXDeviceManager instance associated with this client
     */
    public synchronized MMXDeviceManager getDeviceManager() {
        MMXManager manager = mManagers.get(MMXDeviceManager.class);
        if (manager == null) {
            manager = new MMXDeviceManager(this, mMessagingHandler);
            mManagers.put(MMXDeviceManager.class, manager);
        }
        return (MMXDeviceManager) manager;
    }

    /**
     * Clears the location information for the current user
     *
     * @throws MMXException
     */
    public void clearLocation() throws MMXException {
        MMXGeoLogger.clearGeoLocaction(this);
    }

    /**
     * Attempts to cancel a pending message with the specified id.  This is
     * client-only functionality and will only work for messages that have not
     * been sent.
     *
     * @param messageId The id of the message to cancel.  This is the value returned by sendMessage()
     * @return true if canceled successfully
     */
    boolean cancelMessage(String messageId) {
        if (messageId == null) {
            Log.w(TAG, "cancelMessage(): cannot cancel a null messageId, returning false.");
            return false;
        }
        return getQueue().removeItem(messageId);
    }

    /**
     * Returns the messaging handler for this MMXClient instance.
     *
     * @return the messaging handler associated with this MMXClient
     */
    Handler getHandler() {
        return mMessagingHandler;
    }

    private String bin2hex(byte[] data) {
        return String.format("%0" + (data.length * 2) + "X", new BigInteger(1, data));
    }
}