xuxinyi il y a 1 an
commit
1fc4332577
64 fichiers modifiés avec 6533 ajouts et 0 suppressions
  1. 15 0
      .gitignore
  2. 3 0
      .idea/.gitignore
  3. 1 0
      .idea/.name
  4. 6 0
      .idea/compiler.xml
  5. 10 0
      .idea/deploymentTargetSelector.xml
  6. 20 0
      .idea/gradle.xml
  7. 25 0
      .idea/jarRepositories.xml
  8. 10 0
      .idea/migrations.xml
  9. 9 0
      .idea/misc.xml
  10. 6 0
      .idea/vcs.xml
  11. 20 0
      README.md
  12. 42 0
      app/build.gradle
  13. 21 0
      app/proguard-rules.pro
  14. 26 0
      app/src/androidTest/java/com/somsakelect/android/mymqtt/ExampleInstrumentedTest.java
  15. 33 0
      app/src/main/AndroidManifest.xml
  16. 306 0
      app/src/main/java/com/somsakelect/android/mymqtt/MainActivity.java
  17. 93 0
      app/src/main/java/com/somsakelect/android/mymqtt/helper/BetterActivityResult.java
  18. 30 0
      app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  19. 170 0
      app/src/main/res/drawable/ic_launcher_background.xml
  20. 84 0
      app/src/main/res/layout/activity_main.xml
  21. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  22. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  23. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  24. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  25. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  26. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  27. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  28. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  29. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  30. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  31. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  32. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  33. 16 0
      app/src/main/res/values-night/themes.xml
  34. 10 0
      app/src/main/res/values/colors.xml
  35. 3 0
      app/src/main/res/values/strings.xml
  36. 16 0
      app/src/main/res/values/themes.xml
  37. 17 0
      app/src/test/java/com/somsakelect/android/mymqtt/ExampleUnitTest.java
  38. 24 0
      build.gradle
  39. 19 0
      gradle.properties
  40. BIN
      gradle/wrapper/gradle-wrapper.jar
  41. 6 0
      gradle/wrapper/gradle-wrapper.properties
  42. 172 0
      gradlew
  43. 84 0
      gradlew.bat
  44. 1 0
      mqtt/.gitignore
  45. 40 0
      mqtt/build.gradle
  46. 0 0
      mqtt/consumer-rules.pro
  47. 21 0
      mqtt/proguard-rules.pro
  48. 26 0
      mqtt/src/androidTest/java/com/somsakelect/android/mqtt/ExampleInstrumentedTest.java
  49. 10 0
      mqtt/src/main/AndroidManifest.xml
  50. 176 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/AlarmPingSender.java
  51. 448 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/DatabaseMessageStore.java
  52. 93 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/MessageStore.java
  53. 1766 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/MqttAndroidClient.java
  54. 1132 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/MqttConnection.java
  55. 44 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/MqttDeliveryTokenAndroid.java
  56. 900 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/MqttService.java
  57. 42 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/MqttServiceBinder.java
  58. 88 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/MqttServiceConstants.java
  59. 242 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/MqttTokenAndroid.java
  60. 45 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/MqttTraceHandler.java
  61. 109 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/ParcelableMqttMessage.java
  62. 23 0
      mqtt/src/main/java/com/somsakelect/android/mqtt/Status.java
  63. 17 0
      mqtt/src/test/java/com/somsakelect/android/mqtt/ExampleUnitTest.java
  64. 3 0
      settings.gradle

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties

+ 3 - 0
.idea/.gitignore

@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml

+ 1 - 0
.idea/.name

@@ -0,0 +1 @@
+My MQTT

+ 6 - 0
.idea/compiler.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <bytecodeTargetLevel target="1.8" />
+  </component>
+</project>

+ 10 - 0
.idea/deploymentTargetSelector.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="deploymentTargetSelector">
+    <selectionStates>
+      <SelectionState runConfigName="app">
+        <option name="selectionMode" value="DROPDOWN" />
+      </SelectionState>
+    </selectionStates>
+  </component>
+</project>

+ 20 - 0
.idea/gradle.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="GradleMigrationSettings" migrationVersion="1" />
+  <component name="GradleSettings">
+    <option name="linkedExternalProjectsSettings">
+      <GradleProjectSettings>
+        <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
+        <option name="modules">
+          <set>
+            <option value="$PROJECT_DIR$" />
+            <option value="$PROJECT_DIR$/app" />
+            <option value="$PROJECT_DIR$/mqtt" />
+          </set>
+        </option>
+        <option name="resolveExternalAnnotations" value="false" />
+      </GradleProjectSettings>
+    </option>
+  </component>
+</project>

+ 25 - 0
.idea/jarRepositories.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RemoteRepositoriesConfiguration">
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Maven Central repository" />
+      <option name="url" value="https://repo1.maven.org/maven2" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="jboss.community" />
+      <option name="name" value="JBoss Community repository" />
+      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="BintrayJCenter" />
+      <option name="name" value="BintrayJCenter" />
+      <option name="url" value="https://jcenter.bintray.com/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="Google" />
+      <option name="name" value="Google" />
+      <option name="url" value="https://dl.google.com/dl/android/maven2/" />
+    </remote-repository>
+  </component>
+</project>

+ 10 - 0
.idea/migrations.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectMigrations">
+    <option name="MigrateToGradleLocalJavaHome">
+      <set>
+        <option value="$PROJECT_DIR$" />
+      </set>
+    </option>
+  </component>
+</project>

+ 9 - 0
.idea/misc.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="corretto-1.8" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/build/classes" />
+  </component>
+  <component name="ProjectType">
+    <option name="id" value="Android" />
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 20 - 0
README.md

@@ -0,0 +1,20 @@
+# My MQTT
+
+The MQTT Android Service is an MQTT client library modified from https://github.com/eclipse/paho.mqtt.android
+because old library don't support Android 12. It hasn't updated in a long time.
+
+## How to use
+1. Download ZIP and Extract All
+2. Import library to your android project and select only mqtt folder
+3. Edit the build.gradle (app level) file like below.
+```
+dependencies {
+  ...
+  implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
+  implementation project(path: ':mqtt')
+}
+```
+4. See example project at https://github.com/ElectApp/MyMQTT/tree/master/app
+
+
+git 

+ 42 - 0
app/build.gradle

@@ -0,0 +1,42 @@
+plugins {
+    id 'com.android.application'
+}
+
+android {
+    compileSdkVersion 31
+    buildToolsVersion "30.0.3"
+
+    defaultConfig {
+        applicationId "com.somsakelect.android.mymqtt"
+        minSdkVersion 23
+        targetSdkVersion 31
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+
+    implementation 'androidx.appcompat:appcompat:1.4.2'
+    implementation 'com.google.android.material:material:1.6.1'
+    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+    testImplementation 'junit:junit:4.+'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+    //MQTT
+    implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
+    implementation project(path: ':mqtt')
+}

+ 21 - 0
app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 26 - 0
app/src/androidTest/java/com/somsakelect/android/mymqtt/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package com.somsakelect.android.mymqtt;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertEquals("com.somsakelect.android.mymqtt", appContext.getPackageName());
+    }
+}

+ 33 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.somsakelect.android.mymqtt">
+
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <!--    Fixed 'java.lang.SecurityException with android.permission.SCHEDULE_EXACT_ALARM on android 14 -->
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.MyMQTT">
+        <activity android:name=".MainActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <service android:name="com.somsakelect.android.mqtt.MqttService" />
+
+    </application>
+
+</manifest>

+ 306 - 0
app/src/main/java/com/somsakelect/android/mymqtt/MainActivity.java

@@ -0,0 +1,306 @@
+package com.somsakelect.android.mymqtt;
+
+import androidx.activity.result.ActivityResult;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.app.AlarmManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.somsakelect.android.mqtt.MqttAndroidClient;
+import com.somsakelect.android.mymqtt.helper.BetterActivityResult;
+
+import org.eclipse.paho.client.mqttv3.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+public class MainActivity extends AppCompatActivity {
+    private MqttAndroidClient mqtt;
+
+    public static final String MQTT_HOST = "60.204.139.57";
+    public static final int MQTT_PORT = 1883;
+    public static final String MQTT_USERNAME = "";
+    public static final String MQTT_PASSWORD = "";
+    public static final String MQTT_URL = "tcp://"+MQTT_HOST+":"+MQTT_PORT;
+    public static final String MQTT_ID = MqttClient.generateClientId();
+
+    private TextView subTv, stTv;
+    private EditText eSubTp, ePubTp, ePubMsg;
+    private static final String TAG = "MainActivity";
+    private static final int SCHEDULE_EXACT_ALARM_CODE = 1433;
+    protected final BetterActivityResult<Intent, ActivityResult> activityLauncher = BetterActivityResult.registerActivityForResult(this);
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        subTv = findViewById(R.id.sub_tv);
+        stTv = findViewById(R.id.st_tv);
+        eSubTp = findViewById(R.id.sub_e);
+        ePubTp = findViewById(R.id.pub_tp_e);
+        ePubMsg = findViewById(R.id.pub_pay_e);
+
+        //MQTT
+        mqtt = new MqttAndroidClient(this, MQTT_URL, MQTT_ID);
+        mqtt.setCallback(new MqttCallbackExtended() {
+            @Override
+            public void connectComplete(boolean reconnect, String serverURI) {
+                Log.w(TAG, "MQTT reconnect..."+reconnect);
+                stTv.setText(reconnect? "Reconnecting...":"Connected");
+            }
+
+            @Override
+            public void connectionLost(Throwable cause) {
+                if (cause!=null){
+                    Log.e(TAG, "MQTT lost..."+cause.getMessage());
+                    String st = "MQTT lost! "+cause.getMessage();
+                    stTv.setText(st);
+                }
+            }
+
+            @Override
+            public void messageArrived(String topic, MqttMessage message) {
+                String mess = message.toString();
+                String log = String.format("MQTT RX [%s]: %s", topic, mess);
+                Log.w(TAG, log);
+                //Debug
+                subTv.setText(mess);
+            }
+
+            @Override
+            public void deliveryComplete(IMqttDeliveryToken token) {
+                Log.w(TAG, "Publish success...");
+                showToast("Publish success");
+            }
+        });
+
+        //Connect
+        findViewById(R.id.con_btn).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                connectMQTT();
+            }
+        });
+
+        //Subscribe
+        findViewById(R.id.sub_btn).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                subscribe(getEnter(eSubTp));
+            }
+        });
+
+        //Publish
+        findViewById(R.id.pub_btn).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                publish(getEnter(ePubTp), getEnter(ePubMsg));
+            }
+        });
+
+        //Try connect
+        connectMQTT();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        Log.w(TAG, "onActivityResult: request="+requestCode+", result="+resultCode);
+        if(requestCode == SCHEDULE_EXACT_ALARM_CODE) {
+            if(resultCode == RESULT_OK) {
+                connectMQTT();
+            } else {
+                showToast("Permission is denied.");
+                stTv.setText("Please grant permission.");
+            }
+        }
+    }
+
+    private void showToast(String message){
+        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
+    }
+
+    private String getEnter(EditText e){
+        return e.getText().toString();
+    }
+
+    private void connectMQTT(){
+        // Check if the SCHEDULE_EXACT_ALARM permission is granted
+        // https://developer.android.com/about/versions/14/changes/schedule-exact-alarms
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            AlarmManager alarmManager = (AlarmManager) getSystemService(Service.ALARM_SERVICE);
+            boolean v = alarmManager.canScheduleExactAlarms();
+            Log.d(TAG, "SCHEDULE_EXACT_ALARM is granted..."+v);
+            if(!v) {
+                //Request permission
+                Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
+                Uri uri = Uri.fromParts("package", getPackageName(), null);
+                intent.setData(uri);
+                startActivityForResult2(intent, SCHEDULE_EXACT_ALARM_CODE);
+                return;
+            }
+        }
+
+        Log.w(TAG, "Connecting MQTT server...");
+        stTv.setText("Connecting...");
+        //Set option
+        MqttConnectOptions options = new MqttConnectOptions();
+        if(!TextUtils.isEmpty(MQTT_USERNAME) && !TextUtils.isEmpty(MQTT_PASSWORD)) {
+            options.setUserName(MQTT_USERNAME);
+            options.setPassword(MQTT_PASSWORD.toCharArray());
+        }
+        options.setAutomaticReconnect(true);
+        options.setCleanSession(true);
+        try {
+            IMqttToken token = mqtt.connect(options);
+            token.setActionCallback(new IMqttActionListener() {
+                @Override
+                public void onSuccess(IMqttToken asyncActionToken) {
+                    Log.w(TAG, "Connect success");
+                    stTv.setText("Connected");
+                    //Subscribe
+                    subscribe(getEnter(eSubTp));
+                }
+
+                @Override
+                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+                    Log.e(TAG, "Error..."+exception.getMessage());
+                    String tsx = "Connect onFailure: "+exception.getMessage();
+                    stTv.setText(tsx);
+                }
+            });
+        }catch (MqttException e){
+            e.printStackTrace();
+
+            String tsx = "Connect MqttException: "+e.getMessage();
+            stTv.setText(tsx);
+        }
+    }
+
+    private void disconnectMQTT() {
+        Log.d(TAG, "Disconnecting MQTT server...");
+        try {
+            IMqttToken token = mqtt.disconnect();
+            token.setActionCallback(new IMqttActionListener() {
+                @Override
+                public void onSuccess(IMqttToken asyncActionToken) {
+                    Log.w(TAG, "Disconnect success...");
+                }
+
+                @Override
+                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+                    Log.e(TAG, "Disconnect failed...");
+                }
+            });
+        }catch (MqttException e){
+            e.printStackTrace();
+            Log.e(TAG, "Error..."+e.getMessage());
+        }
+    }
+
+    private void subscribe(@NonNull String topic){
+        Log.w(TAG, "Try to subscribe "+topic+" topics");
+        //Connect
+        if (!mqtt.isConnected()){
+            showToast("Please connect before retry again"); return;
+        }
+        //Action
+        if (TextUtils.isEmpty(topic)){
+            showToast("Please enter topic!"); return;
+        }
+        topic = topic.trim();
+        try {
+            //Set
+            IMqttToken token = mqtt.subscribe(topic, 0);
+            //Check result
+            token.setActionCallback(new IMqttActionListener() {
+                @Override
+                public void onSuccess(IMqttToken asyncActionToken) {
+                    Log.w(TAG, "Subscribed..."
+                            + Arrays.toString(asyncActionToken.getTopics()));
+
+                    showToast("Subscribed");
+                }
+
+                @Override
+                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+                    Log.e(TAG, "Subscribe failed..."
+                            +Arrays.toString(asyncActionToken.getTopics()));
+
+                    showToast("Subscribe error!");
+                }
+            });
+        }catch (MqttException e){
+            e.printStackTrace();
+
+            showToast(e.getMessage());
+        }
+    }
+
+    private void unsubscribe(@NonNull String topic){
+        try {
+            //Set
+            IMqttToken token = mqtt.unsubscribe(topic);
+            //Check result
+            token.setActionCallback(new IMqttActionListener() {
+                @Override
+                public void onSuccess(IMqttToken asyncActionToken) {
+                    Log.w(TAG, "UnSubscribed..."
+                            + Arrays.toString(asyncActionToken.getTopics()));
+                }
+
+                @Override
+                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+                    Log.e(TAG, "UnSubscribed failed..."
+                            +Arrays.toString(asyncActionToken.getTopics()));
+                }
+            });
+        }catch (MqttException e){
+            e.printStackTrace();
+        }
+    }
+
+    public void publish(@NonNull String topic, @NonNull String payload){
+        //Connect
+        if (!mqtt.isConnected()){
+            showToast("Please connect before retry again"); return;
+        }
+        //Action
+        if (TextUtils.isEmpty(topic) || TextUtils.isEmpty(payload)){
+            showToast("Please enter topic and payload!"); return;
+        }
+        topic = topic.trim();
+        payload = payload.trim();
+        try {
+            byte[] encodedPayload = payload.getBytes(StandardCharsets.UTF_8);
+            MqttMessage message = new MqttMessage(encodedPayload);
+            mqtt.publish(topic, message);
+        } catch (MqttException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void startActivityForResult2(final Intent intent, final int requestCode){
+        activityLauncher.launch(intent, new BetterActivityResult.OnActivityResult<ActivityResult>() {
+            @Override
+            public void onActivityResult(ActivityResult result) {
+                MainActivity.this.onActivityResult(requestCode, result.getResultCode(), result.getData());
+            }
+        });
+    }
+}

+ 93 - 0
app/src/main/java/com/somsakelect/android/mymqtt/helper/BetterActivityResult.java

@@ -0,0 +1,93 @@
+package com.somsakelect.android.mymqtt.helper;
+
+import android.content.Intent;
+
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultCaller;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContract;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+//Thank: https://stackoverflow.com/a/63654043/9681342
+public class BetterActivityResult<Input, Result> {
+    /**
+     * Register activity result using a {@link ActivityResultContract} and an in-place activity result callback like
+     * the default approach. You can still customise callback using {@link #launch(Object, OnActivityResult)}.
+     */
+    @NonNull
+    public static <Input, Result> BetterActivityResult<Input, Result> registerForActivityResult(
+            @NonNull ActivityResultCaller caller,
+            @NonNull ActivityResultContract<Input, Result> contract,
+            @Nullable OnActivityResult<Result> onActivityResult) {
+        return new BetterActivityResult<>(caller, contract, onActivityResult);
+    }
+
+    /**
+     * Same as {@link #registerForActivityResult(ActivityResultCaller, ActivityResultContract, OnActivityResult)} except
+     * the last argument is set to {@code null}.
+     */
+    @NonNull
+    public static <Input, Result> BetterActivityResult<Input, Result> registerForActivityResult(
+            @NonNull ActivityResultCaller caller,
+            @NonNull ActivityResultContract<Input, Result> contract) {
+        return registerForActivityResult(caller, contract, null);
+    }
+
+    /**
+     * Specialised method for launching new activities.
+     */
+    @NonNull
+    public static BetterActivityResult<Intent, ActivityResult> registerActivityForResult(
+            @NonNull ActivityResultCaller caller) {
+        return registerForActivityResult(caller, new ActivityResultContracts.StartActivityForResult());
+    }
+
+    /**
+     * Callback interface
+     */
+    public interface OnActivityResult<O> {
+        /**
+         * Called after receiving a result from the target activity
+         */
+        void onActivityResult(O result);
+    }
+
+    private final ActivityResultLauncher<Input> launcher;
+    @Nullable
+    private OnActivityResult<Result> onActivityResult;
+
+    private BetterActivityResult(@NonNull ActivityResultCaller caller,
+                                 @NonNull ActivityResultContract<Input, Result> contract,
+                                 @Nullable OnActivityResult<Result> onActivityResult) {
+        this.onActivityResult = onActivityResult;
+        this.launcher = caller.registerForActivityResult(contract, this::callOnActivityResult);
+    }
+
+    public void setOnActivityResult(@Nullable OnActivityResult<Result> onActivityResult) {
+        this.onActivityResult = onActivityResult;
+    }
+
+    /**
+     * Launch activity, same as {@link ActivityResultLauncher#launch(Object)} except that it allows a callback
+     * executed after receiving a result from the target activity.
+     */
+    public void launch(Input input, @Nullable OnActivityResult<Result> onActivityResult) {
+        if (onActivityResult != null) {
+            this.onActivityResult = onActivityResult;
+        }
+        launcher.launch(input);
+    }
+
+    /**
+     * Same as {@link #launch(Object, OnActivityResult)} with last parameter set to {@code null}.
+     */
+    public void launch(Input input) {
+        launch(input, this.onActivityResult);
+    }
+
+    private void callOnActivityResult(Result result) {
+        if (onActivityResult != null) onActivityResult.onActivityResult(result);
+    }
+}

+ 30 - 0
app/src/main/res/drawable-v24/ic_launcher_foreground.xml

@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="85.84757"
+                android:endY="92.4963"
+                android:startX="42.9492"
+                android:startY="49.59793"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000" />
+</vector>

+ 170 - 0
app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>

+ 84 - 0
app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="16dp"
+    android:orientation="vertical"
+    tools:context=".MainActivity">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="16dp"
+        android:text="My MQTT Test"
+        android:textSize="22sp"
+        android:textColor="@color/black"
+        android:textStyle="bold"
+        android:gravity="center"/>
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Subscribe topic" />
+
+    <EditText
+        android:id="@+id/sub_e"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="MyMQTT/Test"
+        android:hint="Enter topic"/>
+
+    <TextView
+        android:id="@+id/sub_tv"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textColor="@color/teal_700"/>
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:text="Publish topic" />
+
+    <EditText
+        android:id="@+id/pub_tp_e"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="MyMQTT/Test"
+        android:hint="Enter topic"/>
+
+    <EditText
+        android:id="@+id/pub_pay_e"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:text="Hello World"
+        android:hint="Enter payload"/>
+
+    <TextView
+        android:id="@+id/st_tv"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <Button
+        android:id="@+id/con_btn"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:text="Connect" />
+
+    <Button
+        android:id="@+id/sub_btn"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Subscribe" />
+
+    <Button
+        android:id="@+id/pub_btn"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Publish" />
+
+</LinearLayout>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 16 - 0
app/src/main/res/values-night/themes.xml

@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Theme.MyMQTT" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+        <!-- Primary brand color. -->
+        <item name="colorPrimary">@color/purple_200</item>
+        <item name="colorPrimaryVariant">@color/purple_700</item>
+        <item name="colorOnPrimary">@color/black</item>
+        <!-- Secondary brand color. -->
+        <item name="colorSecondary">@color/teal_200</item>
+        <item name="colorSecondaryVariant">@color/teal_200</item>
+        <item name="colorOnSecondary">@color/black</item>
+        <!-- Status bar color. -->
+        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+        <!-- Customize your theme here. -->
+    </style>
+</resources>

+ 10 - 0
app/src/main/res/values/colors.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="purple_200">#FFBB86FC</color>
+    <color name="purple_500">#FF6200EE</color>
+    <color name="purple_700">#FF3700B3</color>
+    <color name="teal_200">#FF03DAC5</color>
+    <color name="teal_700">#FF018786</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">My MQTT</string>
+</resources>

+ 16 - 0
app/src/main/res/values/themes.xml

@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Theme.MyMQTT" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+        <!-- Primary brand color. -->
+        <item name="colorPrimary">@color/purple_500</item>
+        <item name="colorPrimaryVariant">@color/purple_700</item>
+        <item name="colorOnPrimary">@color/white</item>
+        <!-- Secondary brand color. -->
+        <item name="colorSecondary">@color/teal_200</item>
+        <item name="colorSecondaryVariant">@color/teal_700</item>
+        <item name="colorOnSecondary">@color/black</item>
+        <!-- Status bar color. -->
+        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+        <!-- Customize your theme here. -->
+    </style>
+</resources>

+ 17 - 0
app/src/test/java/com/somsakelect/android/mymqtt/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.somsakelect.android.mymqtt;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 24 - 0
build.gradle

@@ -0,0 +1,24 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath "com.android.tools.build:gradle:4.1.3"
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 19 - 0
gradle.properties

@@ -0,0 +1,19 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true

BIN
gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Tue Jul 05 13:22:54 ICT 2022
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip

+ 172 - 0
gradlew

@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"

+ 84 - 0
gradlew.bat

@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 1 - 0
mqtt/.gitignore

@@ -0,0 +1 @@
+/build

+ 40 - 0
mqtt/build.gradle

@@ -0,0 +1,40 @@
+plugins {
+    id 'com.android.library'
+}
+
+android {
+    compileSdkVersion 31
+    buildToolsVersion "30.0.3"
+
+    defaultConfig {
+        minSdkVersion 23
+        targetSdkVersion 31
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+
+    implementation 'androidx.appcompat:appcompat:1.4.2'
+    implementation 'com.google.android.material:material:1.6.1'
+    testImplementation 'junit:junit:4.+'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+    implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
+    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+}

+ 0 - 0
mqtt/consumer-rules.pro


+ 21 - 0
mqtt/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 26 - 0
mqtt/src/androidTest/java/com/somsakelect/android/mqtt/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package com.somsakelect.android.mqtt;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertEquals("com.somsakelect.android.mqtt.test", appContext.getPackageName());
+    }
+}

+ 10 - 0
mqtt/src/main/AndroidManifest.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.somsakelect.android.mqtt">
+
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+</manifest>

+ 176 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/AlarmPingSender.java

@@ -0,0 +1,176 @@
+package com.somsakelect.android.mqtt;
+/*
+ * This scripts is modified from https://github.com/eclipse/paho.mqtt.android/tree/master/org.eclipse.paho.android.service
+ * for fixed error below on Android 12:
+ * Targeting S+ (version 10000 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified
+ * when creating a PendingIntent
+ *
+ * Modified by Somsak Elect, 2022-07-06
+* */
+import android.annotation.SuppressLint;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+
+import org.eclipse.paho.client.mqttv3.IMqttActionListener;
+import org.eclipse.paho.client.mqttv3.IMqttToken;
+import org.eclipse.paho.client.mqttv3.MqttPingSender;
+import org.eclipse.paho.client.mqttv3.internal.ClientComms;
+
+/**
+ * Default ping sender implementation on Android. It is based on AlarmManager.
+ *
+ * <p>This class implements the {@link MqttPingSender} pinger interface
+ * allowing applications to send ping packet to server every keep alive interval.
+ * </p>
+ *
+ * @see MqttPingSender
+ */
+class AlarmPingSender implements MqttPingSender {
+    // Identifier for Intents, log messages, etc..
+    private static final String TAG = "AlarmPingSender";
+
+    // TODO: Add log.
+    private ClientComms comms;
+    private MqttService service;
+    private BroadcastReceiver alarmReceiver;
+    private AlarmPingSender that;
+    private PendingIntent pendingIntent;
+    private volatile boolean hasStarted = false;
+
+    public AlarmPingSender(MqttService service) {
+        if (service == null) {
+            throw new IllegalArgumentException(
+                    "Neither service nor client can be null.");
+        }
+        this.service = service;
+        that = this;
+    }
+
+    @Override
+    public void init(ClientComms comms) {
+        this.comms = comms;
+        this.alarmReceiver = new AlarmReceiver();
+    }
+
+    @Override
+    public void start() {
+        String action = MqttServiceConstants.PING_SENDER
+                + comms.getClient().getClientId();
+        Log.d(TAG, "Register alarmreceiver to MqttService"+ action);
+        service.registerReceiver(alarmReceiver, new IntentFilter(action));
+
+        pendingIntent = PendingIntent.getBroadcast(service, 0, new Intent(
+                action), Build.VERSION.SDK_INT>=Build.VERSION_CODES.S?
+                PendingIntent.FLAG_IMMUTABLE:PendingIntent.FLAG_UPDATE_CURRENT);
+
+        schedule(comms.getKeepAlive());
+        hasStarted = true;
+    }
+
+    @Override
+    public void stop() {
+
+        Log.d(TAG, "Unregister alarmreceiver to MqttService"+comms.getClient().getClientId());
+        if(hasStarted){
+            if(pendingIntent != null){
+                // Cancel Alarm.
+                AlarmManager alarmManager = (AlarmManager) service.getSystemService(Service.ALARM_SERVICE);
+                alarmManager.cancel(pendingIntent);
+            }
+
+            hasStarted = false;
+            try{
+                service.unregisterReceiver(alarmReceiver);
+            }catch(IllegalArgumentException e){
+                //Ignore unregister errors.
+            }
+        }
+    }
+
+    @Override
+    public void schedule(long delayInMilliseconds) {
+        long nextAlarmInMilliseconds = System.currentTimeMillis()
+                + delayInMilliseconds;
+        Log.d(TAG, "Schedule next alarm at " + nextAlarmInMilliseconds);
+        AlarmManager alarmManager = (AlarmManager) service
+                .getSystemService(Service.ALARM_SERVICE);
+
+        if(Build.VERSION.SDK_INT >= 23){
+            // In SDK 23 and above, dosing will prevent setExact, setExactAndAllowWhileIdle will force
+            // the device to run this task whilst dosing.
+            Log.d(TAG, "Alarm scheule using setExactAndAllowWhileIdle, next: " + delayInMilliseconds);
+            alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextAlarmInMilliseconds,
+                    pendingIntent);
+        } else if (Build.VERSION.SDK_INT >= 19) {
+            Log.d(TAG, "Alarm scheule using setExact, delay: " + delayInMilliseconds);
+            alarmManager.setExact(AlarmManager.RTC_WAKEUP, nextAlarmInMilliseconds,
+                    pendingIntent);
+        } else {
+            alarmManager.set(AlarmManager.RTC_WAKEUP, nextAlarmInMilliseconds,
+                    pendingIntent);
+        }
+    }
+
+    /*
+     * This class sends PingReq packet to MQTT broker
+     */
+    class AlarmReceiver extends BroadcastReceiver {
+        private WakeLock wakelock;
+        private final String wakeLockTag = MqttServiceConstants.PING_WAKELOCK
+                + that.comms.getClient().getClientId();
+
+        @Override
+        @SuppressLint("Wakelock")
+        public void onReceive(Context context, Intent intent) {
+            // According to the docs, "Alarm Manager holds a CPU wake lock as
+            // long as the alarm receiver's onReceive() method is executing.
+            // This guarantees that the phone will not sleep until you have
+            // finished handling the broadcast.", but this class still get
+            // a wake lock to wait for ping finished.
+
+            Log.d(TAG, "Sending Ping at:" + System.currentTimeMillis());
+
+            PowerManager pm = (PowerManager) service
+                    .getSystemService(Service.POWER_SERVICE);
+            wakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, wakeLockTag);
+            wakelock.acquire();
+
+            // Assign new callback to token to execute code after PingResq
+            // arrives. Get another wakelock even receiver already has one,
+            // release it until ping response returns.
+            IMqttToken token = comms.checkForActivity(new IMqttActionListener() {
+
+                @Override
+                public void onSuccess(IMqttToken asyncActionToken) {
+                    Log.d(TAG, "Success. Release lock(" + wakeLockTag + "):"
+                            + System.currentTimeMillis());
+                    //Release wakelock when it is done.
+                    wakelock.release();
+                }
+
+                @Override
+                public void onFailure(IMqttToken asyncActionToken,
+                                      Throwable exception) {
+                    Log.d(TAG, "Failure. Release lock(" + wakeLockTag + "):"
+                            + System.currentTimeMillis());
+                    //Release wakelock when it is done.
+                    wakelock.release();
+                }
+            });
+
+
+            if (token == null && wakelock.isHeld()) {
+                wakelock.release();
+            }
+        }
+    }
+}

+ 448 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/DatabaseMessageStore.java

@@ -0,0 +1,448 @@
+package com.somsakelect.android.mqtt;
+
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+
+import java.util.Iterator;
+
+/**
+ * Implementation of the {@link MessageStore} interface, using a SQLite database
+ *
+ */
+class DatabaseMessageStore implements MessageStore {
+
+    // TAG used for indentify trace data etc.
+    private static final String TAG = "DatabaseMessageStore";
+
+    // One "private" database column name
+    // The other database column names are defined in MqttServiceConstants
+    private static final String MTIMESTAMP = "mtimestamp";
+
+    // the name of the table in the database to which we will save messages
+    private static final String ARRIVED_MESSAGE_TABLE_NAME = "MqttArrivedMessageTable";
+
+    // the database
+    private SQLiteDatabase db = null;
+
+    // a SQLiteOpenHelper specific for this database
+    private MQTTDatabaseHelper mqttDb = null;
+
+    // a place to send trace data
+    private MqttTraceHandler traceHandler = null;
+
+    /**
+     * We need a SQLiteOpenHelper to handle database creation and updating
+     *
+     */
+    private static class MQTTDatabaseHelper extends SQLiteOpenHelper {
+        // TAG used for indentify trace data etc.
+        private static final String TAG = "MQTTDatabaseHelper";
+
+        private static final String DATABASE_NAME = "mqttAndroidService.db";
+
+        // database version, used to recognise when we need to upgrade
+        // (delete and recreate)
+        private static final int DATABASE_VERSION = 1;
+
+        // a place to send trace data
+        private MqttTraceHandler traceHandler = null;
+
+        /**
+         * Constructor.
+         *
+         * @param traceHandler
+         * @param context
+         */
+        public MQTTDatabaseHelper(MqttTraceHandler traceHandler, Context context) {
+            super(context, DATABASE_NAME, null, DATABASE_VERSION);
+            this.traceHandler = traceHandler;
+        }
+
+        /**
+         * When the database is (re)created, create our table
+         *
+         * @param database
+         */
+        @Override
+        public void onCreate(SQLiteDatabase database) {
+            String createArrivedTableStatement = "CREATE TABLE "
+                    + ARRIVED_MESSAGE_TABLE_NAME + "("
+                    + MqttServiceConstants.MESSAGE_ID + " TEXT PRIMARY KEY, "
+                    + MqttServiceConstants.CLIENT_HANDLE + " TEXT, "
+                    + MqttServiceConstants.DESTINATION_NAME + " TEXT, "
+                    + MqttServiceConstants.PAYLOAD + " BLOB, "
+                    + MqttServiceConstants.QOS + " INTEGER, "
+                    + MqttServiceConstants.RETAINED + " TEXT, "
+                    + MqttServiceConstants.DUPLICATE + " TEXT, " + MTIMESTAMP
+                    + " INTEGER" + ");";
+            traceHandler.traceDebug(TAG, "onCreate {"
+                    + createArrivedTableStatement + "}");
+            try {
+                database.execSQL(createArrivedTableStatement);
+                traceHandler.traceDebug(TAG, "created the table");
+            } catch (SQLException e) {
+                traceHandler.traceException(TAG, "onCreate", e);
+                throw e;
+            }
+        }
+
+        /**
+         * To upgrade the database, drop and recreate our table
+         *
+         * @param db
+         *            the database
+         * @param oldVersion
+         *            ignored
+         * @param newVersion
+         *            ignored
+         */
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            traceHandler.traceDebug(TAG, "onUpgrade");
+            try {
+                db.execSQL("DROP TABLE IF EXISTS " + ARRIVED_MESSAGE_TABLE_NAME);
+            } catch (SQLException e) {
+                traceHandler.traceException(TAG, "onUpgrade", e);
+                throw e;
+            }
+            onCreate(db);
+            traceHandler.traceDebug(TAG, "onUpgrade complete");
+        }
+    }
+
+    /**
+     * Constructor - create a DatabaseMessageStore to store arrived MQTT message
+     *
+     * @param service
+     *            our parent MqttService
+     * @param context
+     *            a context to use for android calls
+     */
+    public DatabaseMessageStore(MqttService service, Context context) {
+        this.traceHandler = service;
+
+        // Open message database
+        mqttDb = new MQTTDatabaseHelper(traceHandler, context);
+
+        // Android documentation suggests that this perhaps
+        // could/should be done in another thread, but as the
+        // database is only one table, I doubt it matters...
+
+        traceHandler.traceDebug(TAG, "DatabaseMessageStore<init> complete");
+    }
+
+    /**
+     * Store an MQTT message
+     *
+     * @param clientHandle
+     *            identifier for the client storing the message
+     * @param topic
+     *            The topic on which the message was published
+     * @param message
+     *            the arrived MQTT message
+     * @return an identifier for the message, so that it can be removed when appropriate
+     */
+    @Override
+    public String storeArrived(String clientHandle, String topic,
+                               MqttMessage message) {
+
+        db = mqttDb.getWritableDatabase();
+
+        traceHandler.traceDebug(TAG, "storeArrived{" + clientHandle + "}, {"
+                + message.toString() + "}");
+
+        byte[] payload = message.getPayload();
+        int qos = message.getQos();
+        boolean retained = message.isRetained();
+        boolean duplicate = message.isDuplicate();
+
+        ContentValues values = new ContentValues();
+        String id = java.util.UUID.randomUUID().toString();
+        values.put(MqttServiceConstants.MESSAGE_ID, id);
+        values.put(MqttServiceConstants.CLIENT_HANDLE, clientHandle);
+        values.put(MqttServiceConstants.DESTINATION_NAME, topic);
+        values.put(MqttServiceConstants.PAYLOAD, payload);
+        values.put(MqttServiceConstants.QOS, qos);
+        values.put(MqttServiceConstants.RETAINED, retained);
+        values.put(MqttServiceConstants.DUPLICATE, duplicate);
+        values.put(MTIMESTAMP, System.currentTimeMillis());
+        try {
+            db.insertOrThrow(ARRIVED_MESSAGE_TABLE_NAME, null, values);
+        } catch (SQLException e) {
+            traceHandler.traceException(TAG, "onUpgrade", e);
+            throw e;
+        }
+        int count = getArrivedRowCount(clientHandle);
+        traceHandler
+                .traceDebug(
+                        TAG,
+                        "storeArrived: inserted message with id of {"
+                                + id
+                                + "} - Number of messages in database for this clientHandle = "
+                                + count);
+        return id;
+    }
+
+    private int getArrivedRowCount(String clientHandle) {
+        int count = 0;
+        String[] projection = {
+                MqttServiceConstants.MESSAGE_ID,
+        };
+        String selection =  MqttServiceConstants.CLIENT_HANDLE + "=?";
+        String[] selectionArgs = new String[1];
+        selectionArgs[0] = clientHandle;
+        Cursor c = db.query(
+                ARRIVED_MESSAGE_TABLE_NAME, // Table Name
+                projection, // The columns to return;
+                selection, // Columns for WHERE Clause
+                selectionArgs , // The values for the WHERE Cause
+                null,  //Don't group the rows
+                null,  // Don't filter by row groups
+                null   // The sort order
+        );
+
+        if (c.moveToFirst()) {
+            count = c.getInt(0);
+        }
+        c.close();
+        return count;
+    }
+
+    /**
+     * Delete an MQTT message.
+     *
+     * @param clientHandle
+     *            identifier for the client which stored the message
+     * @param id
+     *            the identifying string returned when the message was stored
+     *
+     * @return true if the message was found and deleted
+     */
+    @Override
+    public boolean discardArrived(String clientHandle, String id) {
+
+        db = mqttDb.getWritableDatabase();
+
+        traceHandler.traceDebug(TAG, "discardArrived{" + clientHandle + "}, {"
+                + id + "}");
+        int rows;
+        String[] selectionArgs = new String[2];
+        selectionArgs[0] = id;
+        selectionArgs[1] = clientHandle;
+
+        try {
+            rows = db.delete(ARRIVED_MESSAGE_TABLE_NAME,
+                    MqttServiceConstants.MESSAGE_ID + "=? AND "
+                            + MqttServiceConstants.CLIENT_HANDLE + "=?",
+                    selectionArgs);
+        } catch (SQLException e) {
+            traceHandler.traceException(TAG, "discardArrived", e);
+            throw e;
+        }
+        if (rows != 1) {
+            traceHandler.traceError(TAG,
+                    "discardArrived - Error deleting message {" + id
+                            + "} from database: Rows affected = " + rows);
+            return false;
+        }
+        int count = getArrivedRowCount(clientHandle);
+        traceHandler
+                .traceDebug(
+                        TAG,
+                        "discardArrived - Message deleted successfully. - messages in db for this clientHandle "
+                                + count);
+        return true;
+    }
+
+    /**
+     * Get an iterator over all messages stored (optionally for a specific client)
+     *
+     * @param clientHandle
+     *            identifier for the client.<br>
+     *            If null, all messages are retrieved
+     * @return iterator of all the arrived MQTT messages
+     */
+    @Override
+    public Iterator<StoredMessage> getAllArrivedMessages(
+            final String clientHandle) {
+        return new Iterator<StoredMessage>() {
+            private Cursor c;
+            private boolean hasNext;
+            private final String[] selectionArgs = {
+                    clientHandle,
+            };
+
+
+            {
+                db = mqttDb.getWritableDatabase();
+                // anonymous initialiser to start a suitable query
+                // and position at the first row, if one exists
+                if (clientHandle == null) {
+                    c = db.query(ARRIVED_MESSAGE_TABLE_NAME,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null,
+                            "mtimestamp ASC");
+                } else {
+                    c = db.query(ARRIVED_MESSAGE_TABLE_NAME,
+                            null,
+                            MqttServiceConstants.CLIENT_HANDLE + "=?",
+                            selectionArgs,
+                            null,
+                            null,
+                            "mtimestamp ASC");
+                }
+                hasNext = c.moveToFirst();
+            }
+
+            @Override
+            public boolean hasNext() {
+                if (!hasNext){
+                    c.close();
+                }
+                return hasNext;
+            }
+
+            @Override
+            public StoredMessage next() {
+                String messageId = c.getString(c
+                        .getColumnIndex(MqttServiceConstants.MESSAGE_ID));
+                String clientHandle = c.getString(c
+                        .getColumnIndex(MqttServiceConstants.CLIENT_HANDLE));
+                String topic = c.getString(c
+                        .getColumnIndex(MqttServiceConstants.DESTINATION_NAME));
+                byte[] payload = c.getBlob(c
+                        .getColumnIndex(MqttServiceConstants.PAYLOAD));
+                int qos = c.getInt(c.getColumnIndex(MqttServiceConstants.QOS));
+                boolean retained = Boolean.parseBoolean(c.getString(c
+                        .getColumnIndex(MqttServiceConstants.RETAINED)));
+                boolean dup = Boolean.parseBoolean(c.getString(c
+                        .getColumnIndex(MqttServiceConstants.DUPLICATE)));
+
+                // build the result
+                MqttMessageHack message = new MqttMessageHack(payload);
+                message.setQos(qos);
+                message.setRetained(retained);
+                message.setDuplicate(dup);
+
+                // move on
+                hasNext = c.moveToNext();
+                return new DbStoredData(messageId, clientHandle, topic, message);
+            }
+
+            @Override
+            public void remove() {
+                throw new UnsupportedOperationException();
+            }
+
+            /* (non-Javadoc)
+             * @see java.lang.Object#finalize()
+             */
+            @Override
+            protected void finalize() throws Throwable {
+                c.close();
+                super.finalize();
+            }
+
+        };
+    }
+
+    /**
+     * Delete all messages (optionally for a specific client)
+     *
+     * @param clientHandle
+     *            identifier for the client.<br>
+     *            If null, all messages are deleted
+     */
+    @Override
+    public void clearArrivedMessages(String clientHandle) {
+
+        db = mqttDb.getWritableDatabase();
+        String[] selectionArgs = new String[1];
+        selectionArgs[0] = clientHandle;
+
+        int rows = 0;
+        if (clientHandle == null) {
+            traceHandler.traceDebug(TAG,
+                    "clearArrivedMessages: clearing the table");
+            rows = db.delete(ARRIVED_MESSAGE_TABLE_NAME, null, null);
+        } else {
+            traceHandler.traceDebug(TAG,
+                    "clearArrivedMessages: clearing the table of "
+                            + clientHandle + " messages");
+            rows = db.delete(ARRIVED_MESSAGE_TABLE_NAME,
+                    MqttServiceConstants.CLIENT_HANDLE + "=?",
+                    selectionArgs);
+
+        }
+        traceHandler.traceDebug(TAG, "clearArrivedMessages: rows affected = "
+                + rows);
+    }
+
+    private class DbStoredData implements StoredMessage {
+        private String messageId;
+        private String clientHandle;
+        private String topic;
+        private MqttMessage message;
+
+        DbStoredData(String messageId, String clientHandle, String topic,
+                     MqttMessage message) {
+            this.messageId = messageId;
+            this.topic = topic;
+            this.message = message;
+        }
+
+        @Override
+        public String getMessageId() {
+            return messageId;
+        }
+
+        @Override
+        public String getClientHandle() {
+            return clientHandle;
+        }
+
+        @Override
+        public String getTopic() {
+            return topic;
+        }
+
+        @Override
+        public MqttMessage getMessage() {
+            return message;
+        }
+    }
+
+    /**
+     * A way to get at the "setDuplicate" method of MqttMessage
+     */
+    private class MqttMessageHack extends MqttMessage {
+
+        public MqttMessageHack(byte[] payload) {
+            super(payload);
+        }
+
+        @Override
+        protected void setDuplicate(boolean dup) {
+            super.setDuplicate(dup);
+        }
+    }
+
+    @Override
+    public void close() {
+        if (this.db!=null)
+            this.db.close();
+
+    }
+
+}

+ 93 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/MessageStore.java

@@ -0,0 +1,93 @@
+package com.somsakelect.android.mqtt;
+
+
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+
+import java.util.Iterator;
+
+/**
+ * <p>
+ * Mechanism for persisting messages until we know they have been received
+ * </p>
+ * <ul>
+ * <li>A Service should store messages as they arrive via
+ * {@link #storeArrived(String, String, MqttMessage)}.
+ * <li>When a message has been passed to the consuming entity,
+ * {@link #discardArrived(String, String)} should be called.
+ * <li>To recover messages which have not been definitely passed to the
+ * consumer, {@link MessageStore#getAllArrivedMessages(String)} is used.
+ * <li>When a clean session is started {@link #clearArrivedMessages(String)} is
+ * used.
+ * </ul>
+ */
+interface MessageStore {
+
+    /**
+     * External representation of a stored message
+     */
+    interface StoredMessage {
+        /**
+         * @return the identifier for the message within the store
+         */
+        String getMessageId();
+
+        /**
+         * @return the identifier of the client which stored this message
+         */
+        String getClientHandle();
+
+        /**
+         * @return the topic on which the message was received
+         */
+        String getTopic();
+
+        /**
+         * @return the identifier of the client which stored this message
+         */
+        MqttMessage getMessage();
+    }
+
+    /**
+     * Store a message and return an identifier for it
+     *
+     * @param clientHandle
+     *            identifier for the client
+     * @param message
+     *            message to be stored
+     * @return a unique identifier for it
+     */
+    String storeArrived(String clientHandle, String Topic,
+                        MqttMessage message);
+
+    /**
+     * Discard a message - called when we are certain that an arrived message
+     * has reached the application.
+     *
+     * @param clientHandle
+     *            identifier for the client
+     * @param id
+     *            id of message to be discarded
+     */
+    boolean discardArrived(String clientHandle, String id);
+
+    /**
+     * Get all the stored messages, usually for a specific client
+     *
+     * @param clientHandle
+     *            identifier for the client - if null, then messages for all
+     *            clients are returned
+     */
+    Iterator<StoredMessage> getAllArrivedMessages(String clientHandle);
+
+    /**
+     * Discard stored messages, usually for a specific client
+     *
+     * @param clientHandle
+     *            identifier for the client - if null, then messages for all
+     *            clients are discarded
+     */
+    void clearArrivedMessages(String clientHandle);
+
+    void close();
+}
+

+ 1766 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/MqttAndroidClient.java

@@ -0,0 +1,1766 @@
+package com.somsakelect.android.mqtt;
+
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.SparseArray;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions;
+import org.eclipse.paho.client.mqttv3.IMqttActionListener;
+import org.eclipse.paho.client.mqttv3.IMqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.IMqttMessageListener;
+import org.eclipse.paho.client.mqttv3.IMqttToken;
+import org.eclipse.paho.client.mqttv3.MqttCallback;
+import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
+import org.eclipse.paho.client.mqttv3.MqttClientPersistence;
+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.MqttPersistenceException;
+import org.eclipse.paho.client.mqttv3.MqttSecurityException;
+import org.eclipse.paho.client.mqttv3.MqttToken;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+
+/**
+ * Enables an android application to communicate with an MQTT server using non-blocking methods.
+ * <p>
+ * Implementation of the MQTT asynchronous client interface {@link IMqttAsyncClient} , using the MQTT
+ * android service to actually interface with MQTT server. It provides android applications a simple programming interface to all features of the MQTT version 3.1
+ * specification including:
+ * </p>
+ * <ul>
+ * <li>connect
+ * <li>publish
+ * <li>subscribe
+ * <li>unsubscribe
+ * <li>disconnect
+ * </ul>
+ */
+public class MqttAndroidClient extends BroadcastReceiver implements
+        IMqttAsyncClient {
+
+    /**
+     *
+     * The Acknowledgment mode for messages received from {@link MqttCallback#messageArrived(String, MqttMessage)}
+     *
+     */
+    public enum Ack {
+        /**
+         * As soon as the {@link MqttCallback#messageArrived(String, MqttMessage)} returns,
+         * the message has been acknowledged as received .
+         */
+        AUTO_ACK,
+        /**
+         * When {@link MqttCallback#messageArrived(String, MqttMessage)} returns, the message
+         * will not be acknowledged as received, the application will have to make an acknowledgment call
+         * to {@link MqttAndroidClient} using {@link MqttAndroidClient#acknowledgeMessage(String)}
+         */
+        MANUAL_ACK
+    }
+
+    private static final String SERVICE_NAME = "com.somsakelect.android.mqtt.MqttService";
+
+    private static final int BIND_SERVICE_FLAG = 0;
+
+    private static final ExecutorService pool = Executors.newCachedThreadPool();
+
+    /**
+     * ServiceConnection to process when we bind to our service
+     */
+    private final class MyServiceConnection implements ServiceConnection {
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder binder) {
+            mqttService = ((MqttServiceBinder) binder).getService();
+            bindedService = true;
+            // now that we have the service available, we can actually
+            // connect...
+            doConnect();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            mqttService = null;
+        }
+    }
+
+    // Listener for when the service is connected or disconnected
+    private final MyServiceConnection serviceConnection = new MyServiceConnection();
+
+    // The Android Service which will process our mqtt calls
+    private MqttService mqttService;
+
+    // An identifier for the underlying client connection, which we can pass to
+    // the service
+    private String clientHandle;
+
+    private Context myContext;
+
+    // We hold the various tokens in a collection and pass identifiers for them
+    // to the service
+    private final SparseArray<IMqttToken> tokenMap = new SparseArray<>();
+    private int tokenNumber = 0;
+
+    // Connection data
+    private final String serverURI;
+    private final String clientId;
+    private MqttClientPersistence persistence = null;
+    private MqttConnectOptions connectOptions;
+    private IMqttToken connectToken;
+
+    // The MqttCallback provided by the application
+    private MqttCallback callback;
+    private MqttTraceHandler traceCallback;
+
+    //The acknowledgment that a message has been processed by the application
+    private final Ack messageAck;
+    private boolean traceEnabled = false;
+
+    private volatile boolean receiverRegistered = false;
+    private volatile boolean bindedService = false;
+
+    /**
+     * Constructor - create an MqttAndroidClient that can be used to communicate with an MQTT server on android
+     *
+     * @param context
+     *            object used to pass context to the callback.
+     * @param serverURI
+     *            specifies the protocol, host name and port to be used to
+     *            connect to an MQTT server
+     * @param clientId
+     *            specifies the name by which this connection should be
+     *            identified to the server
+     */
+    public MqttAndroidClient(Context context, String serverURI,
+                             String clientId) {
+        this(context, serverURI, clientId, null, Ack.AUTO_ACK);
+    }
+
+    /**
+     * Constructor - create an MqttAndroidClient that can be used to communicate
+     * with an MQTT server on android
+     *
+     * @param ctx
+     *            Application's context
+     * @param serverURI
+     *            specifies the protocol, host name and port to be used to
+     *            connect to an MQTT server
+     * @param clientId
+     *            specifies the name by which this connection should be
+     *            identified to the server
+     * @param ackType
+     *            how the application wishes to acknowledge a message has been
+     *            processed
+     */
+    public MqttAndroidClient(Context ctx, String serverURI, String clientId, Ack ackType) {
+        this(ctx, serverURI, clientId, null, ackType);
+    }
+
+    /**
+     * Constructor - create an MqttAndroidClient that can be used to communicate
+     * with an MQTT server on android
+     *
+     * @param ctx
+     *            Application's context
+     * @param serverURI
+     *            specifies the protocol, host name and port to be used to
+     *            connect to an MQTT server
+     * @param clientId
+     *            specifies the name by which this connection should be
+     *            identified to the server
+     * @param persistence
+     *            The object to use to store persisted data
+     */
+    public MqttAndroidClient(Context ctx, String serverURI, String clientId, MqttClientPersistence persistence) {
+        this(ctx, serverURI, clientId, persistence, Ack.AUTO_ACK);
+    }
+
+    /**
+     * Constructor- create an MqttAndroidClient that can be used to communicate
+     * with an MQTT server on android
+     *
+     * @param context
+     *            used to pass context to the callback.
+     * @param serverURI
+     *            specifies the protocol, host name and port to be used to
+     *            connect to an MQTT server
+     * @param clientId
+     *            specifies the name by which this connection should be
+     *            identified to the server
+     * @param persistence
+     *            the persistence class to use to store in-flight message. If
+     *            null then the default persistence mechanism is used
+     * @param ackType
+     *            how the application wishes to acknowledge a message has been
+     *            processed.
+     */
+    public MqttAndroidClient(Context context, String serverURI,
+                             String clientId, MqttClientPersistence persistence, Ack ackType) {
+        myContext = context;
+        this.serverURI = serverURI;
+        this.clientId = clientId;
+        this.persistence = persistence;
+        messageAck = ackType;
+    }
+
+    /**
+     * Determines if this client is currently connected to the server.
+     *
+     * @return <code>true</code> if connected, <code>false</code> otherwise.
+     */
+    @Override
+    public boolean isConnected() {
+
+        return clientHandle != null && mqttService != null && mqttService.isConnected(clientHandle);
+    }
+
+    /**
+     * Returns the client ID used by this client.
+     * <p>
+     * All clients connected to the same server or server farm must have a
+     * unique ID.
+     * </p>
+     *
+     * @return the client ID used by this client.
+     */
+    @Override
+    public String getClientId() {
+        return clientId;
+    }
+
+    /**
+     * Returns the URI address of the server used by this client.
+     * <p>
+     * The format of the returned String is the same as that used on the
+     * constructor.
+     * </p>
+     *
+     * @return the server's address, as a URI String.
+     */
+    @Override
+    public String getServerURI() {
+        return serverURI;
+    }
+
+    /**
+     * Close the client. Releases all resource associated with the client. After
+     * the client has been closed it cannot be reused. For instance attempts to
+     * connect will fail.
+     *
+     */
+    @Override
+    public void close() {
+        if(mqttService != null){
+            if (clientHandle == null) {
+                clientHandle = mqttService.getClient(serverURI, clientId, myContext.getApplicationInfo().packageName,persistence);
+            }
+            mqttService.close(clientHandle);
+        }
+    }
+
+    /**
+     * Connects to an MQTT server using the default options.
+     * <p>
+     * The default options are specified in {@link MqttConnectOptions} class.
+     * </p>
+     *
+     * @throws MqttException
+     *             for any connected problems
+     * @return token used to track and wait for the connect to complete. The
+     *         token will be passed to the callback methods if a callback is
+     *         set.
+     * @see #connect(MqttConnectOptions, Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken connect() throws MqttException {
+        return connect(null, null);
+    }
+
+
+    /**
+     * Connects to an MQTT server using the provided connect options.
+     * <p>
+     * The connection will be established using the options specified in the
+     * {@link MqttConnectOptions} parameter.
+     * </p>
+     *
+     * @param options
+     *            a set of connection parameters that override the defaults.
+     * @throws MqttException
+     *             for any connected problems
+     * @return token used to track and wait for the connect to complete. The
+     *         token will be passed to any callback that has been set.
+     * @see #connect(MqttConnectOptions, Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken connect(MqttConnectOptions options) throws MqttException {
+        return connect(options, null, null);
+    }
+
+    /**
+     * Connects to an MQTT server using the default options.
+     * <p>
+     * The default options are specified in {@link MqttConnectOptions} class.
+     * </p>
+     *
+     * @param userContext
+     *            optional object used to pass context to the callback. Use null
+     *            if not required.
+     * @param callback
+     *            optional listener that will be notified when the connect
+     *            completes. Use null if not required.
+     * @throws MqttException
+     *             for any connected problems
+     * @return token used to track and wait for the connect to complete. The
+     *         token will be passed to any callback that has been set.
+     * @see #connect(MqttConnectOptions, Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken connect(Object userContext, IMqttActionListener callback)
+            throws MqttException {
+        return connect(new MqttConnectOptions(), userContext, callback);
+    }
+
+    /**
+     * Connects to an MQTT server using the specified options.
+     * <p>
+     * The server to connect to is specified on the constructor. It is
+     * recommended to call {@link #setCallback(MqttCallback)} prior to
+     * connecting in order that messages destined for the client can be accepted
+     * as soon as the client is connected.
+     * </p>
+     *
+     * <p>
+     * The method returns control before the connect completes. Completion can
+     * be tracked by:
+     * </p>
+     * <ul>
+     * <li>Waiting on the returned token {@link IMqttToken#waitForCompletion()}
+     * or</li>
+     * <li>Passing in a callback {@link IMqttActionListener}</li>
+     * </ul>
+     *
+     *
+     * @param options
+     *            a set of connection parameters that override the defaults.
+     * @param userContext
+     *            optional object for used to pass context to the callback. Use
+     *            null if not required.
+     * @param callback
+     *            optional listener that will be notified when the connect
+     *            completes. Use null if not required.
+     * @return token used to track and wait for the connect to complete. The
+     *         token will be passed to any callback that has been set.
+     * @throws MqttException
+     *             for any connected problems, including communication errors
+     */
+
+    @Override
+    public IMqttToken connect(MqttConnectOptions options, Object userContext,
+                              IMqttActionListener callback) throws MqttException {
+
+        IMqttToken token = new MqttTokenAndroid(this, userContext,
+                callback);
+
+        connectOptions = options;
+        connectToken = token;
+
+        /*
+         * The actual connection depends on the service, which we start and bind
+         * to here, but which we can't actually use until the serviceConnection
+         * onServiceConnected() method has run (asynchronously), so the
+         * connection itself takes place in the onServiceConnected() method
+         */
+        if (mqttService == null) { // First time - must bind to the service
+            Intent serviceStartIntent = new Intent();
+            serviceStartIntent.setClassName(myContext, SERVICE_NAME);
+            Object service = myContext.startService(serviceStartIntent);
+            if (service == null) {
+                IMqttActionListener listener = token.getActionCallback();
+                if (listener != null) {
+                    listener.onFailure(token, new RuntimeException(
+                            "cannot start service " + SERVICE_NAME));
+                }
+            }
+
+            // We bind with BIND_SERVICE_FLAG (0), leaving us the manage the lifecycle
+            // until the last time it is stopped by a call to stopService()
+            myContext.bindService(serviceStartIntent, serviceConnection,
+                    Context.BIND_AUTO_CREATE);
+
+            if (!receiverRegistered) registerReceiver(this);
+        }
+        else {
+            pool.execute(new Runnable() {
+
+                @Override
+                public void run() {
+                    doConnect();
+
+                    //Register receiver to show shoulder tap.
+                    if (!receiverRegistered) registerReceiver(MqttAndroidClient.this);
+                }
+
+            });
+        }
+
+        return token;
+    }
+
+    private void registerReceiver(BroadcastReceiver receiver) {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(MqttServiceConstants.CALLBACK_TO_ACTIVITY);
+        LocalBroadcastManager.getInstance(myContext).registerReceiver(receiver, filter);
+        receiverRegistered = true;
+    }
+
+    /**
+     * Actually do the mqtt connect operation
+     */
+    private void doConnect() {
+        if (clientHandle == null) {
+            clientHandle = mqttService.getClient(serverURI, clientId,myContext.getApplicationInfo().packageName,
+                    persistence);
+        }
+        mqttService.setTraceEnabled(traceEnabled);
+        mqttService.setTraceCallbackId(clientHandle);
+
+        String activityToken = storeToken(connectToken);
+        try {
+            mqttService.connect(clientHandle, connectOptions, null,
+                    activityToken);
+        }
+        catch (MqttException e) {
+            IMqttActionListener listener = connectToken.getActionCallback();
+            if (listener != null) {
+                listener.onFailure(connectToken, e);
+            }
+        }
+    }
+
+    /**
+     * Disconnects from the server.
+     * <p>
+     * An attempt is made to quiesce the client allowing outstanding work to
+     * complete before disconnecting. It will wait for a maximum of 30 seconds
+     * for work to quiesce before disconnecting. This method must not be called
+     * from inside {@link MqttCallback} methods.
+     * </p>
+     *
+     * @return token used to track and wait for disconnect to complete. The
+     *         token will be passed to any callback that has been set.
+     * @throws MqttException
+     *             for problems encountered while disconnecting
+     * @see #disconnect(long, Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken disconnect() throws MqttException {
+        IMqttToken token = new MqttTokenAndroid(this, null,
+                null);
+        String activityToken = storeToken(token);
+        mqttService.disconnect(clientHandle, null, activityToken);
+        return token;
+    }
+
+    /**
+     * Disconnects from the server.
+     * <p>
+     * An attempt is made to quiesce the client allowing outstanding work to
+     * complete before disconnecting. It will wait for a maximum of the
+     * specified quiesce time for work to complete before disconnecting. This
+     * method must not be called from inside {@link MqttCallback} methods.
+     * </p>
+     *
+     * @param quiesceTimeout
+     *            the amount of time in milliseconds to allow for existing work
+     *            to finish before disconnecting. A value of zero or less means
+     *            the client will not quiesce.
+     * @return token used to track and wait for disconnect to complete. The
+     *         token will be passed to the callback methods if a callback is
+     *         set.
+     * @throws MqttException
+     *             for problems encountered while disconnecting
+     * @see #disconnect(long, Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken disconnect(long quiesceTimeout) throws MqttException {
+        IMqttToken token = new MqttTokenAndroid(this, null,
+                null);
+        String activityToken = storeToken(token);
+        mqttService.disconnect(clientHandle, quiesceTimeout, null,
+                activityToken);
+        return token;
+    }
+
+    /**
+     * Disconnects from the server.
+     * <p>
+     * An attempt is made to quiesce the client allowing outstanding work to
+     * complete before disconnecting. It will wait for a maximum of 30 seconds
+     * for work to quiesce before disconnecting. This method must not be called
+     * from inside {@link MqttCallback} methods.
+     * </p>
+     *
+     * @param userContext
+     *            optional object used to pass context to the callback. Use null
+     *            if not required.
+     * @param callback
+     *            optional listener that will be notified when the disconnect
+     *            completes. Use null if not required.
+     * @return token used to track and wait for the disconnect to complete. The
+     *         token will be passed to any callback that has been set.
+     * @throws MqttException
+     *             for problems encountered while disconnecting
+     * @see #disconnect(long, Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken disconnect(Object userContext,
+                                 IMqttActionListener callback) throws MqttException {
+        IMqttToken token = new MqttTokenAndroid(this, userContext,
+                callback);
+        String activityToken = storeToken(token);
+        mqttService.disconnect(clientHandle, null, activityToken);
+        return token;
+    }
+
+    /**
+     * Disconnects from the server.
+     * <p>
+     * The client will wait for {@link MqttCallback} methods to complete. It
+     * will then wait for up to the quiesce timeout to allow for work which has
+     * already been initiated to complete. For instance when a QoS 2 message has
+     * started flowing to the server but the QoS 2 flow has not completed.It
+     * prevents new messages being accepted and does not send any messages that
+     * have been accepted but not yet started delivery across the network to the
+     * server. When work has completed or after the quiesce timeout, the client
+     * will disconnect from the server. If the cleanSession flag was set to
+     * false and next time it is also set to false in the connection, the
+     * messages made in QoS 1 or 2 which were not previously delivered will be
+     * delivered this time.
+     * </p>
+     * <p>
+     * This method must not be called from inside {@link MqttCallback} methods.
+     * </p>
+     * <p>
+     * The method returns control before the disconnect completes. Completion
+     * can be tracked by:
+     * </p>
+     * <ul>
+     * <li>Waiting on the returned token {@link IMqttToken#waitForCompletion()}
+     * or</li>
+     * <li>Passing in a callback {@link IMqttActionListener}</li>
+     * </ul>
+     *
+     * @param quiesceTimeout
+     *            the amount of time in milliseconds to allow for existing work
+     *            to finish before disconnecting. A value of zero or less means
+     *            the client will not quiesce.
+     * @param userContext
+     *            optional object used to pass context to the callback. Use null
+     *            if not required.
+     * @param callback
+     *            optional listener that will be notified when the disconnect
+     *            completes. Use null if not required.
+     * @return token used to track and wait for the disconnect to complete. The
+     *         token will be passed to any callback that has been set.
+     * @throws MqttException
+     *             for problems encountered while disconnecting
+     */
+    @Override
+    public IMqttToken disconnect(long quiesceTimeout, Object userContext,
+                                 IMqttActionListener callback) throws MqttException {
+        IMqttToken token = new MqttTokenAndroid(this, userContext,
+                callback);
+        String activityToken = storeToken(token);
+        mqttService.disconnect(clientHandle, quiesceTimeout, null,
+                activityToken);
+        return token;
+    }
+
+    /**
+     * Publishes a message to a topic on the server.
+     * <p>
+     * A convenience method, which will create a new {@link MqttMessage} object
+     * with a byte array payload and the specified QoS, and then publish it.
+     * </p>
+     *
+     * @param topic
+     *            to deliver the message to, for example "finance/stock/ibm".
+     * @param payload
+     *            the byte array to use as the payload
+     * @param qos
+     *            the Quality of Service to deliver the message at. Valid values
+     *            are 0, 1 or 2.
+     * @param retained
+     *            whether or not this message should be retained by the server.
+     * @return token used to track and wait for the publish to complete. The
+     *         token will be passed to any callback that has been set.
+     * @throws MqttPersistenceException
+     *             when a problem occurs storing the message
+     * @throws IllegalArgumentException
+     *             if value of QoS is not 0, 1 or 2.
+     * @throws MqttException
+     *             for other errors encountered while publishing the message.
+     *             For instance, too many messages are being processed.
+     * @see #publish(String, MqttMessage, Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttDeliveryToken publish(String topic, byte[] payload, int qos,
+                                      boolean retained) throws MqttException, MqttPersistenceException {
+        return publish(topic, payload, qos, retained, null, null);
+    }
+
+    /**
+     * Publishes a message to a topic on the server. Takes an
+     * {@link MqttMessage} message and delivers it to the server at the
+     * requested quality of service.
+     *
+     * @param topic
+     *            to deliver the message to, for example "finance/stock/ibm".
+     * @param message
+     *            to deliver to the server
+     * @return token used to track and wait for the publish to complete. The
+     *         token will be passed to any callback that has been set.
+     * @throws MqttPersistenceException
+     *             when a problem occurs storing the message
+     * @throws IllegalArgumentException
+     *             if value of QoS is not 0, 1 or 2.
+     * @throws MqttException
+     *             for other errors encountered while publishing the message.
+     *             For instance client not connected.
+     * @see #publish(String, MqttMessage, Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttDeliveryToken publish(String topic, MqttMessage message)
+            throws MqttException, MqttPersistenceException {
+        return publish(topic, message, null, null);
+    }
+
+    /**
+     * Publishes a message to a topic on the server.
+     * <p>
+     * A convenience method, which will create a new {@link MqttMessage} object
+     * with a byte array payload, the specified QoS and retained, then publish it.
+     * </p>
+     *
+     * @param topic
+     *            to deliver the message to, for example "finance/stock/ibm".
+     * @param payload
+     *            the byte array to use as the payload
+     * @param qos
+     *            the Quality of Service to deliver the message at. Valid values
+     *            are 0, 1 or 2.
+     * @param retained
+     *            whether or not this message should be retained by the server.
+     * @param userContext
+     *            optional object used to pass context to the callback. Use null
+     *            if not required.
+     * @param callback
+     *            optional listener that will be notified when message delivery
+     *            has completed to the requested quality of service
+     * @return token used to track and wait for the publish to complete. The
+     *         token will be passed to any callback that has been set.
+     * @throws MqttPersistenceException
+     *             when a problem occurs storing the message
+     * @throws IllegalArgumentException
+     *             if value of QoS is not 0, 1 or 2.
+     * @throws MqttException
+     *             for other errors encountered while publishing the message.
+     *             For instance client not connected.
+     * @see #publish(String, MqttMessage, Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttDeliveryToken publish(String topic, byte[] payload, int qos,
+                                      boolean retained, Object userContext, IMqttActionListener callback)
+            throws MqttException, MqttPersistenceException {
+
+        MqttMessage message = new MqttMessage(payload);
+        message.setQos(qos);
+        message.setRetained(retained);
+        MqttDeliveryTokenAndroid token = new MqttDeliveryTokenAndroid(
+                this, userContext, callback, message);
+        String activityToken = storeToken(token);
+        IMqttDeliveryToken internalToken = mqttService.publish(clientHandle,
+                topic, payload, qos, retained, null, activityToken);
+        token.setDelegate(internalToken);
+        return token;
+    }
+
+    /**
+     * Publishes a message to a topic on the server.
+     * <p>
+     * Once this method has returned cleanly, the message has been accepted for
+     * publication by the client and will be delivered on a background thread.
+     * In the event the connection fails or the client stops, Messages will be
+     * delivered to the requested quality of service once the connection is
+     * re-established to the server on condition that:
+     * </p>
+     * <ul>
+     * <li>The connection is re-established with the same clientID
+     * <li>The original connection was made with (@link
+     * MqttConnectOptions#setCleanSession(boolean)} set to false
+     * <li>The connection is re-established with (@link
+     * MqttConnectOptions#setCleanSession(boolean)} set to false
+     * <li>Depending when the failure occurs QoS 0 messages may not be
+     * delivered.
+     * </ul>
+     *
+     * <p>
+     * When building an application, the design of the topic tree should take
+     * into account the following principles of topic name syntax and semantics:
+     * </p>
+     *
+     * <ul>
+     * <li>A topic must be at least one character long.</li>
+     * <li>Topic names are case sensitive. For example, <em>ACCOUNTS</em> and
+     * <em>Accounts</em> are two different topics.</li>
+     * <li>Topic names can include the space character. For example,
+     * <em>Accounts
+     * 	payable</em> is a valid topic.</li>
+     * <li>A leading "/" creates a distinct topic. For example,
+     * <em>/finance</em> is different from <em>finance</em>. <em>/finance</em>
+     * matches "+/+" and "/+", but not "+".</li>
+     * <li>Do not include the null character (Unicode <em>\x0000</em>) in any topic.</li>
+     * </ul>
+     *
+     * <p>
+     * The following principles apply to the construction and content of a topic
+     * tree:
+     * </p>
+     *
+     * <ul>
+     * <li>The length is limited to 64k but within that there are no limits to
+     * the number of levels in a topic tree.</li>
+     * <li>There can be any number of root nodes; that is, there can be any
+     * number of topic trees.</li>
+     * </ul>
+     * <p>
+     * The method returns control before the publish completes. Completion can
+     * be tracked by:
+     * </p>
+     * <ul>
+     * <li>Setting an {@link IMqttAsyncClient#setCallback(MqttCallback)} where
+     * the {@link MqttCallback#deliveryComplete(IMqttDeliveryToken)} method will
+     * be called.</li>
+     * <li>Waiting on the returned token {@link MqttToken#waitForCompletion()}
+     * or</li>
+     * <li>Passing in a callback {@link IMqttActionListener} to this method</li>
+     * </ul>
+     *
+     * @param topic
+     *            to deliver the message to, for example "finance/stock/ibm".
+     * @param message
+     *            to deliver to the server
+     * @param userContext
+     *            optional object used to pass context to the callback. Use null
+     *            if not required.
+     * @param callback
+     *            optional listener that will be notified when message delivery
+     *            has completed to the requested quality of service
+     * @return token used to track and wait for the publish to complete. The
+     *         token will be passed to callback methods if set.
+     * @throws MqttPersistenceException
+     *             when a problem occurs storing the message
+     * @throws IllegalArgumentException
+     *             if value of QoS is not 0, 1 or 2.
+     * @throws MqttException
+     *             for other errors encountered while publishing the message.
+     *             For instance, client not connected.
+     * @see MqttMessage
+     */
+    @Override
+    public IMqttDeliveryToken publish(String topic, MqttMessage message,
+                                      Object userContext, IMqttActionListener callback)
+            throws MqttException, MqttPersistenceException {
+        MqttDeliveryTokenAndroid token = new MqttDeliveryTokenAndroid(
+                this, userContext, callback, message);
+        String activityToken = storeToken(token);
+        IMqttDeliveryToken internalToken = mqttService.publish(clientHandle,
+                topic, message, null, activityToken);
+        token.setDelegate(internalToken);
+        return token;
+    }
+
+    /**
+     * Subscribe to a topic, which may include wildcards.
+     *
+     * @param topic
+     *            the topic to subscribe to, which can include wildcards.
+     * @param qos
+     *            the maximum quality of service at which to subscribe. Messages
+     *            published at a lower quality of service will be received at
+     *            the published QoS. Messages published at a higher quality of
+     *            service will be received using the QoS specified on the
+     *            subscription.
+     * @return token used to track and wait for the subscribe to complete. The
+     *         token will be passed to callback methods if set.
+     * @throws MqttSecurityException
+     *             for security related problems
+     * @throws MqttException
+     *             for non security related problems
+     *
+     * @see #subscribe(String[], int[], Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken subscribe(String topic, int qos) throws MqttException,
+            MqttSecurityException {
+        return subscribe(topic, qos, null, null);
+    }
+
+    /**
+     * Subscribe to multiple topics, each topic may include wildcards.
+     *
+     * <p>
+     * Provides an optimized way to subscribe to multiple topics compared to
+     * subscribing to each one individually.
+     * </p>
+     *
+     * @param topic
+     *            one or more topics to subscribe to, which can include
+     *            wildcards
+     * @param qos
+     *            the maximum quality of service at which to subscribe. Messages
+     *            published at a lower quality of service will be received at
+     *            the published QoS. Messages published at a higher quality of
+     *            service will be received using the QoS specified on the
+     *            subscription.
+     * @return token used to track and wait for the subscription to complete. The
+     *         token will be passed to callback methods if set.
+     * @throws MqttSecurityException
+     *             for security related problems
+     * @throws MqttException
+     *             for non security related problems
+     *
+     * @see #subscribe(String[], int[], Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken subscribe(String[] topic, int[] qos)
+            throws MqttException, MqttSecurityException {
+        return subscribe(topic, qos, null, null);
+    }
+
+    /**
+     * Subscribe to a topic, which may include wildcards.
+     *
+     * @param topic
+     *            the topic to subscribe to, which can include wildcards.
+     * @param qos
+     *            the maximum quality of service at which to subscribe. Messages
+     *            published at a lower quality of service will be received at
+     *            the published QoS. Messages published at a higher quality of
+     *            service will be received using the QoS specified on the
+     *            subscription.
+     * @param userContext
+     *            optional object used to pass context to the callback. Use null
+     *            if not required.
+     * @param callback
+     *            optional listener that will be notified when subscribe has
+     *            completed
+     * @return token used to track and wait for the subscribe to complete. The
+     *         token will be passed to callback methods if set.
+     * @throws MqttException
+     *             if there was an error when registering the subscription.
+     *
+     * @see #subscribe(String[], int[], Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken subscribe(String topic, int qos, Object userContext,
+                                IMqttActionListener callback) throws MqttException {
+        IMqttToken token = new MqttTokenAndroid(this, userContext,
+                callback, new String[]{topic});
+        String activityToken = storeToken(token);
+        mqttService.subscribe(clientHandle, topic, qos, null, activityToken);
+        return token;
+    }
+
+    /**
+     * Subscribes to multiple topics, each topic may include wildcards.
+     * <p>
+     * Provides an optimized way to subscribe to multiple topics compared to
+     * subscribing to each one individually.
+     * </p>
+     * <p>
+     * The {@link #setCallback(MqttCallback)} method should be called before
+     * this method, otherwise any received messages will be discarded.
+     * </p>
+     * <p>
+     * If (@link MqttConnectOptions#setCleanSession(boolean)} was set to true,
+     * when connecting to the server, the subscription remains in place until
+     * either:
+     * </p>
+     * <ul>
+     * <li>The client disconnects</li>
+     * <li>An unsubscribe method is called to unsubscribe the topic</li>
+     * </ul>
+     * <p>
+     * If (@link MqttConnectOptions#setCleanSession(boolean)} was set to false,
+     * when connecting to the server, the subscription remains in place
+     * until either:
+     * </p>
+     * <ul>
+     * <li>An unsubscribe method is called to unsubscribe the topic</li>
+     * <li>The next time the client connects with cleanSession set to true
+     * </ul>
+     * <p>With cleanSession set to false the MQTT server will store messages
+     * on behalf of the client when the client is not connected. The next time
+     * the client connects with the <b>same client ID</b> the server will
+     * deliver the stored messages to the client.
+     * </p>
+     *
+     * <p>
+     * The "topic filter" string is used when subscription may contain special
+     * characters, which allows you to subscribe to multiple topics at once.
+     * <dl>
+     * <dt>Topic level separator</dt>
+     * <dd>The forward slash (/) is used to separate each level within a topic
+     * tree and provide a hierarchical structure to the topic space. The use of
+     * the topic level separator is significant when the two wildcard characters
+     * are encountered in topics specified by subscribers.</dd>
+     *
+     * <dt>Multi-level wildcard</dt>
+     * <dd>
+     * <p>
+     * The number sign (#) is a wildcard character that matches any number of
+     * levels within a topic. For example, if you subscribe to <span><span
+     * class="filepath">finance/stock/ibm/#</span></span>, you receive messages
+     * on these topics:
+     * </p>
+     * <ul>
+     *     <li><pre>finance/stock/ibm</pre></li>
+     *     <li><pre>finance/stock/ibm/closingprice</pre></li>
+     *     <li><pre>finance/stock/ibm/currentprice</pre></li>
+     * </ul>
+     *
+     * <p>
+     * The multi-level wildcard can represent zero or more levels. Therefore,
+     * <em>finance/#</em> can also match the singular <em>finance</em>, where
+     * <em>#</em> represents zero levels. The topic level separator is
+     * meaningless in this context, because there are no levels to separate.
+     * </p>
+     *
+     * <p>
+     * The <span>multi-level</span> wildcard can be specified only on its own or
+     * next to the topic level separator character. Therefore, <em>#</em> and
+     * <em>finance/#</em> are both valid, but <em>finance#</em> is not valid.
+     * <span>The multi-level wildcard must be the last character used within the
+     * topic tree. For example, <em>finance/#</em> is valid but
+     * <em>finance/#/closingprice</em> is not valid.</span>
+     * </p>
+     * </dd>
+     *
+     * <dt>Single-level wildcard</dt>
+     * <dd>
+     * <p>
+     * The plus sign (+) is a wildcard character that matches only one topic
+     * level. For example, <em>finance/stock/+</em> matches
+     * <em>finance/stock/ibm</em> and <em>finance/stock/xyz</em>, but not
+     * <em>finance/stock/ibm/closingprice</em>. Also, because the single-level
+     * wildcard matches only a single level, <em>finance/+</em> does not match
+     * <em>finance</em>.
+     * </p>
+     *
+     * <p>
+     * Use the single-level wildcard at any level in the topic tree, and in
+     * conjunction with the multilevel wildcard. Specify the single-level
+     * wildcard next to the topic level separator, except when it is specified
+     * on its own. Therefore, <em>+</em> and <em>finance/+</em> are both valid,
+     * but <em>finance+</em> is not valid. <span>The single-level wildcard can
+     * be used at the end of the topic tree or within the topic tree. For
+     * example, <em>finance/+</em> and <em>finance/+/ibm</em> are both
+     * valid.</span>
+     * </p>
+     * </dd>
+     * </dl>
+     * <p>
+     * The method returns control before the subscribe completes. Completion can
+     * be tracked by:
+     * </p>
+     * <ul>
+     * <li>Waiting on the supplied token {@link MqttToken#waitForCompletion()}
+     * or</li>
+     * <li>Passing in a callback {@link IMqttActionListener} to this method</li>
+     * </ul>
+     *
+     * @param topic
+     *            one or more topics to subscribe to, which can include
+     *            wildcards
+     * @param qos
+     *            the maximum quality of service to subscribe each topic
+     *            at.Messages published at a lower quality of service will be
+     *            received at the published QoS. Messages published at a higher
+     *            quality of service will be received using the QoS specified on
+     *            the subscription.
+     * @param userContext
+     *            optional object used to pass context to the callback. Use null
+     *            if not required.
+     * @param callback
+     *            optional listener that will be notified when subscribe has
+     *            completed
+     * @return token used to track and wait for the subscribe to complete. The
+     *         token will be passed to callback methods if set.
+     * @throws MqttException
+     *             if there was an error registering the subscription.
+     * @throws IllegalArgumentException
+     *             if the two supplied arrays are not the same size.
+     */
+    @Override
+    public IMqttToken subscribe(String[] topic, int[] qos, Object userContext,
+                                IMqttActionListener callback) throws MqttException {
+        IMqttToken token = new MqttTokenAndroid(this, userContext,
+                callback, topic);
+        String activityToken = storeToken(token);
+        mqttService.subscribe(clientHandle, topic, qos, null, activityToken);
+        return token;
+    }
+
+    /**
+     * Subscribe to a topic, which may include wildcards.
+     *
+     * @see #subscribe(String[], int[], Object, IMqttActionListener)
+     *
+     * @param topicFilter the topic to subscribe to, which can include wildcards.
+     * @param qos the maximum quality of service at which to subscribe. Messages
+     * published at a lower quality of service will be received at the published
+     * QoS.  Messages published at a higher quality of service will be received using
+     * the QoS specified on the subscribe.
+     * @param userContext optional object used to pass context to the callback. Use
+     * null if not required.
+     * @param callback optional listener that will be notified when subscribe
+     * has completed
+     * @param messageListener a callback to handle incoming messages
+     * @return token used to track and wait for the subscribe to complete. The token
+     * will be passed to callback methods if set.
+     * @throws MqttException if there was an error registering the subscription.
+     */
+    public IMqttToken subscribe(String topicFilter, int qos, Object userContext, IMqttActionListener callback, IMqttMessageListener messageListener) throws MqttException {
+
+        return subscribe(new String[] {topicFilter}, new int[] {qos}, userContext, callback, new IMqttMessageListener[] {messageListener});
+    }
+
+    /**
+     * Subscribe to a topic, which may include wildcards.
+     *
+     * @see #subscribe(String[], int[], Object, IMqttActionListener)
+     *
+     * @param topicFilter the topic to subscribe to, which can include wildcards.
+     * @param qos the maximum quality of service at which to subscribe. Messages
+     * published at a lower quality of service will be received at the published
+     * QoS.  Messages published at a higher quality of service will be received using
+     * the QoS specified on the subscribe.
+     * @param messageListener a callback to handle incoming messages
+     * @return token used to track and wait for the subscribe to complete. The token
+     * will be passed to callback methods if set.
+     * @throws MqttException if there was an error registering the subscription.
+     */
+    public IMqttToken subscribe(String topicFilter, int qos, IMqttMessageListener messageListener) throws MqttException {
+
+        return subscribe(topicFilter, qos, null, null, messageListener);
+    }
+
+
+    /**
+     * Subscribe to multiple topics, each of which may include wildcards.
+     *
+     * <p>Provides an optimized way to subscribe to multiple topics compared to
+     * subscribing to each one individually.</p>
+     *
+     * @see #subscribe(String[], int[], Object, IMqttActionListener)
+     *
+     * @param topicFilters one or more topics to subscribe to, which can include wildcards
+     * @param qos the maximum quality of service at which to subscribe. Messages
+     * published at a lower quality of service will be received at the published
+     * QoS.  Messages published at a higher quality of service will be received using
+     * the QoS specified on the subscribe.
+     * @param messageListeners an array of callbacks to handle incoming messages
+     * @return token used to track and wait for the subscribe to complete. The token
+     * will be passed to callback methods if set.
+     * @throws MqttException if there was an error registering the subscription.
+     */
+    public IMqttToken subscribe(String[] topicFilters, int[] qos, IMqttMessageListener[] messageListeners) throws MqttException {
+
+        return subscribe(topicFilters, qos, null, null, messageListeners);
+    }
+
+
+    /**
+     * Subscribe to multiple topics, each of which may include wildcards.
+     *
+     * <p>Provides an optimized way to subscribe to multiple topics compared to
+     * subscribing to each one individually.</p>
+     *
+     * @see #subscribe(String[], int[], Object, IMqttActionListener)
+     *
+     * @param topicFilters one or more topics to subscribe to, which can include wildcards
+     * @param qos the maximum quality of service at which to subscribe. Messages
+     * published at a lower quality of service will be received at the published
+     * QoS.  Messages published at a higher quality of service will be received using
+     * the QoS specified on the subscribe.
+     * @param userContext optional object used to pass context to the callback. Use
+     * null if not required.
+     * @param callback optional listener that will be notified when subscribe
+     * has completed
+     * @param messageListeners an array of callbacks to handle incoming messages
+     * @return token used to track and wait for the subscribe to complete. The token
+     * will be passed to callback methods if set.
+     * @throws MqttException if there was an error registering the subscription.
+     */
+    public IMqttToken subscribe(String[] topicFilters, int[] qos, Object userContext, IMqttActionListener callback, IMqttMessageListener[] messageListeners) throws MqttException {
+        IMqttToken token = new MqttTokenAndroid(this, userContext, callback, topicFilters);
+        String activityToken = storeToken(token);
+        mqttService.subscribe(clientHandle, topicFilters, qos, null, activityToken, messageListeners);
+
+        return null;
+    }
+
+
+    /**
+     * Requests the server unsubscribe the client from a topic.
+     *
+     * @param topic
+     *            the topic to unsubscribe from. It must match a topic specified
+     *            on an earlier subscribe.
+     * @return token used to track and wait for the unsubscribe to complete. The
+     *         token will be passed to callback methods if set.
+     * @throws MqttException
+     *             if there was an error unregistering the subscription.
+     *
+     * @see #unsubscribe(String[], Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken unsubscribe(String topic) throws MqttException {
+        return unsubscribe(topic, null, null);
+    }
+
+    /**
+     * Requests the server to unsubscribe the client from one or more topics.
+     *
+     * @param topic
+     *            one or more topics to unsubscribe from. Each topic must match
+     *            one specified on an earlier subscription.
+     * @return token used to track and wait for the unsubscribe to complete. The
+     *         token will be passed to callback methods if set.
+     * @throws MqttException
+     *             if there was an error unregistering the subscription.
+     *
+     * @see #unsubscribe(String[], Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken unsubscribe(String[] topic) throws MqttException {
+        return unsubscribe(topic, null, null);
+    }
+
+    /**
+     * Requests the server to unsubscribe the client from a topics.
+     *
+     * @param topic
+     *            the topic to unsubscribe from. It must match a topic specified
+     *            on an earlier subscribe.
+     * @param userContext
+     *            optional object used to pass context to the callback. Use null
+     *            if not required.
+     * @param callback
+     *            optional listener that will be notified when unsubscribe has
+     *            completed
+     * @return token used to track and wait for the unsubscribe to complete. The
+     *         token will be passed to callback methods if set.
+     * @throws MqttException
+     *             if there was an error unregistering the subscription.
+     *
+     * @see #unsubscribe(String[], Object, IMqttActionListener)
+     */
+    @Override
+    public IMqttToken unsubscribe(String topic, Object userContext,
+                                  IMqttActionListener callback) throws MqttException {
+        IMqttToken token = new MqttTokenAndroid(this, userContext,
+                callback);
+        String activityToken = storeToken(token);
+        mqttService.unsubscribe(clientHandle, topic, null, activityToken);
+        return token;
+    }
+
+    /**
+     * Requests the server to unsubscribe the client from one or more topics.
+     * <p>
+     * Unsubcribing is the opposite of subscribing. When the server receives the
+     * unsubscribe request it looks to see if it can find a matching
+     * subscription for the client and then removes it. After this point the
+     * server will send no more messages to the client for this subscription.
+     * </p>
+     * <p>
+     * The topic(s) specified on the unsubscribe must match the topic(s)
+     * specified in the original subscribe request for the unsubscribe to
+     * succeed
+     * </p>
+     * <p>
+     * The method returns control before the unsubscribe completes. Completion
+     * can be tracked by:
+     * </p>
+     * <ul>
+     * <li>Waiting on the returned token {@link MqttToken#waitForCompletion()}
+     * or</li>
+     * <li>Passing in a callback {@link IMqttActionListener} to this method</li>
+     * </ul>
+     *
+     * @param topic
+     *            one or more topics to unsubscribe from. Each topic must match
+     *            one specified on an earlier subscription.
+     * @param userContext
+     *            optional object used to pass context to the callback. Use null
+     *            if not required.
+     * @param callback
+     *            optional listener that will be notified when unsubscribe has
+     *            completed
+     * @return token used to track and wait for the unsubscribe to complete. The
+     *         token will be passed to callback methods if set.
+     * @throws MqttException
+     *             if there was an error unregistering the subscription.
+     */
+    @Override
+    public IMqttToken unsubscribe(String[] topic, Object userContext,
+                                  IMqttActionListener callback) throws MqttException {
+        IMqttToken token = new MqttTokenAndroid(this, userContext,
+                callback);
+        String activityToken = storeToken(token);
+        mqttService.unsubscribe(clientHandle, topic, null, activityToken);
+        return token;
+    }
+
+    @Override
+    public boolean removeMessage(IMqttDeliveryToken token) throws MqttException {
+        return false;
+    }
+
+    /**
+     * Returns the delivery tokens for any outstanding publish operations.
+     * <p>
+     * If a client has been restarted and there are messages that were in the
+     * process of being delivered when the client stopped, this method returns a
+     * token for each in-flight message to enable the delivery to be tracked.
+     * Alternately the {@link MqttCallback#deliveryComplete(IMqttDeliveryToken)}
+     * callback can be used to track the delivery of outstanding messages.
+     * </p>
+     * <p>
+     * If a client connects with cleanSession true then there will be no
+     * delivery tokens as the cleanSession option deletes all earlier state. For
+     * state to be remembered the client must connect with cleanSession set to
+     * false
+     * </P>
+     *
+     * @return zero or more delivery tokens
+     */
+    @Override
+    public IMqttDeliveryToken[] getPendingDeliveryTokens() {
+        return mqttService.getPendingDeliveryTokens(clientHandle);
+    }
+
+    /**
+     * Sets a callback listener to use for events that happen asynchronously.
+     * <p>
+     * There are a number of events that the listener will be notified about.
+     * These include:
+     * </p>
+     * <ul>
+     * <li>A new message has arrived and is ready to be processed</li>
+     * <li>The connection to the server has been lost</li>
+     * <li>Delivery of a message to the server has completed</li>
+     * </ul>
+     * <p>
+     * Other events that track the progress of an individual operation such as
+     * connect and subscribe can be tracked using the {@link MqttToken} returned
+     * from each non-blocking method or using setting a
+     * {@link IMqttActionListener} on the non-blocking method.
+     * <p>
+     *
+     * @param callback
+     *            which will be invoked for certain asynchronous events
+     *
+     * @see MqttCallback
+     */
+    @Override
+    public void setCallback(MqttCallback callback) {
+        this.callback = callback;
+
+    }
+
+    /**
+     * identify the callback to be invoked when making tracing calls back into
+     * the Activity
+     *
+     * @param traceCallback handler
+     */
+    public void setTraceCallback(MqttTraceHandler traceCallback) {
+        this.traceCallback = traceCallback;
+        // mqttService.setTraceCallbackId(traceCallbackId);
+    }
+
+    /**
+     * turn tracing on and off
+     *
+     * @param traceEnabled
+     *            set <code>true</code> to enable trace, otherwise, set
+     *            <code>false</code> to disable trace
+     *
+     */
+    public void setTraceEnabled(boolean traceEnabled) {
+        this.traceEnabled = traceEnabled;
+        if (mqttService !=null)
+            mqttService.setTraceEnabled(traceEnabled);
+    }
+
+    /**
+     * <p>
+     * Process incoming Intent objects representing the results of operations
+     * and asynchronous activities such as message received
+     * </p>
+     * <p>
+     * <strong>Note:</strong> This is only a public method because the Android
+     * APIs require such.<br>
+     * This method should not be explicitly invoked.
+     * </p>
+     */
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Bundle data = intent.getExtras();
+
+        String handleFromIntent = data
+                .getString(MqttServiceConstants.CALLBACK_CLIENT_HANDLE);
+
+        if ((handleFromIntent == null)
+                || (!handleFromIntent.equals(clientHandle))) {
+            return;
+        }
+
+        String action = data.getString(MqttServiceConstants.CALLBACK_ACTION);
+
+        if (MqttServiceConstants.CONNECT_ACTION.equals(action)) {
+            connectAction(data);
+        }
+        else if (MqttServiceConstants.CONNECT_EXTENDED_ACTION.equals(action)){
+            connectExtendedAction(data);
+        }
+        else if (MqttServiceConstants.MESSAGE_ARRIVED_ACTION.equals(action)) {
+            messageArrivedAction(data);
+        }
+        else if (MqttServiceConstants.SUBSCRIBE_ACTION.equals(action)) {
+            subscribeAction(data);
+        }
+        else if (MqttServiceConstants.UNSUBSCRIBE_ACTION.equals(action)) {
+            unSubscribeAction(data);
+        }
+        else if (MqttServiceConstants.SEND_ACTION.equals(action)) {
+            sendAction(data);
+        }
+        else if (MqttServiceConstants.MESSAGE_DELIVERED_ACTION.equals(action)) {
+            messageDeliveredAction(data);
+        }
+        else if (MqttServiceConstants.ON_CONNECTION_LOST_ACTION
+                .equals(action)) {
+            connectionLostAction(data);
+        }
+        else if (MqttServiceConstants.DISCONNECT_ACTION.equals(action)) {
+            disconnected(data);
+        }
+        else if (MqttServiceConstants.TRACE_ACTION.equals(action)) {
+            traceAction(data);
+        }else{
+            mqttService.traceError(MqttService.TAG, "Callback action doesn't exist.");
+        }
+
+    }
+
+    /**
+     * Acknowledges a message received on the
+     * {@link MqttCallback#messageArrived(String, MqttMessage)}
+     *
+     * @param messageId
+     *            the messageId received from the MqttMessage (To access this
+     *            field you need to cast {@link MqttMessage} to
+     *            {@link ParcelableMqttMessage})
+     * @return whether or not the message was successfully acknowledged
+     */
+    public boolean acknowledgeMessage(String messageId) {
+        if (messageAck == Ack.MANUAL_ACK) {
+            Status status = mqttService.acknowledgeMessageArrival(clientHandle, messageId);
+            return status == Status.OK;
+        }
+        return false;
+
+    }
+
+    public void messageArrivedComplete(int messageId, int qos) throws MqttException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setManualAcks(boolean manualAcks) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void reconnect() throws MqttException {
+
+    }
+
+    /**
+     * Process the results of a connection
+     *
+     * @param data
+     */
+    private void connectAction(Bundle data) {
+        IMqttToken token = connectToken;
+        removeMqttToken(data);
+
+        simpleAction(token, data);
+    }
+
+
+
+    /**
+     * Process a notification that we have disconnected
+     *
+     * @param data
+     */
+    private void disconnected(Bundle data) {
+        clientHandle = null; // avoid reuse!
+        IMqttToken token = removeMqttToken(data);
+        if (token != null) {
+            ((MqttTokenAndroid) token).notifyComplete();
+        }
+        if (callback != null) {
+            callback.connectionLost(null);
+        }
+    }
+
+    /**
+     * Process a Connection Lost notification
+     *
+     * @param data
+     */
+    private void connectionLostAction(Bundle data) {
+        if (callback != null) {
+            Exception reason = (Exception) data
+                    .getSerializable(MqttServiceConstants.CALLBACK_EXCEPTION);
+            callback.connectionLost(reason);
+        }
+    }
+
+    private void connectExtendedAction(Bundle data){
+        // This is called differently from a normal connect
+
+        if(callback instanceof MqttCallbackExtended){
+            boolean reconnect = data.getBoolean(MqttServiceConstants.CALLBACK_RECONNECT, false);
+            String serverURI = data.getString(MqttServiceConstants.CALLBACK_SERVER_URI);
+            ((MqttCallbackExtended) callback).connectComplete(reconnect, serverURI);
+        }
+
+    }
+
+    /**
+     * Common processing for many notifications
+     *
+     * @param token
+     *            the token associated with the action being undertake
+     * @param data
+     *            the result data
+     */
+    private void simpleAction(IMqttToken token, Bundle data) {
+        if (token != null) {
+            Status status = (Status) data
+                    .getSerializable(MqttServiceConstants.CALLBACK_STATUS);
+            if (status == Status.OK) {
+                ((MqttTokenAndroid) token).notifyComplete();
+            }
+            else {
+                Exception exceptionThrown = (Exception) data.getSerializable(MqttServiceConstants.CALLBACK_EXCEPTION);
+                ((MqttTokenAndroid) token).notifyFailure(exceptionThrown);
+            }
+        } else {
+            mqttService.traceError(MqttService.TAG, "simpleAction : token is null");
+        }
+    }
+
+    /**
+     * Process notification of a publish(send) operation
+     *
+     * @param data
+     */
+    private void sendAction(Bundle data) {
+        IMqttToken token = getMqttToken(data); // get, don't remove - will
+        // remove on delivery
+        simpleAction(token, data);
+    }
+
+    /**
+     * Process notification of a subscribe operation
+     *
+     * @param data
+     */
+    private void subscribeAction(Bundle data) {
+        IMqttToken token = removeMqttToken(data);
+        simpleAction(token, data);
+    }
+
+    /**
+     * Process notification of an unsubscribe operation
+     *
+     * @param data
+     */
+    private void unSubscribeAction(Bundle data) {
+        IMqttToken token = removeMqttToken(data);
+        simpleAction(token, data);
+    }
+
+    /**
+     * Process notification of a published message having been delivered
+     *
+     * @param data
+     */
+    private void messageDeliveredAction(Bundle data) {
+        IMqttToken token = removeMqttToken(data);
+        if (token != null) {
+            if (callback != null) {
+                Status status = (Status) data
+                        .getSerializable(MqttServiceConstants.CALLBACK_STATUS);
+                if (status == Status.OK && token instanceof IMqttDeliveryToken) {
+                    callback.deliveryComplete((IMqttDeliveryToken) token);
+                }
+            }
+        }
+    }
+
+    /**
+     * Process notification of a message's arrival
+     *
+     * @param data
+     */
+    private void messageArrivedAction(Bundle data) {
+        if (callback != null) {
+            String messageId = data
+                    .getString(MqttServiceConstants.CALLBACK_MESSAGE_ID);
+            String destinationName = data
+                    .getString(MqttServiceConstants.CALLBACK_DESTINATION_NAME);
+
+            ParcelableMqttMessage message = data
+                    .getParcelable(MqttServiceConstants.CALLBACK_MESSAGE_PARCEL);
+            try {
+                if (messageAck == Ack.AUTO_ACK) {
+                    callback.messageArrived(destinationName, message);
+                    mqttService.acknowledgeMessageArrival(clientHandle, messageId);
+                }
+                else {
+                    message.messageId = messageId;
+                    callback.messageArrived(destinationName, message);
+                }
+
+                // let the service discard the saved message details
+            }
+            catch (Exception e) {
+                // Swallow the exception
+            }
+        }
+    }
+
+    /**
+     * Process trace action - pass trace data back to the callback
+     *
+     * @param data
+     */
+    private void traceAction(Bundle data) {
+
+        if (traceCallback != null) {
+            String severity = data.getString(MqttServiceConstants.CALLBACK_TRACE_SEVERITY);
+            String message =  data.getString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE);
+            String tag = data.getString(MqttServiceConstants.CALLBACK_TRACE_TAG);
+            if (MqttServiceConstants.TRACE_DEBUG.equals(severity))
+                traceCallback.traceDebug(tag, message);
+            else if (MqttServiceConstants.TRACE_ERROR.equals(severity))
+                traceCallback.traceError(tag, message);
+            else
+            {
+                Exception e = (Exception) data.getSerializable(MqttServiceConstants.CALLBACK_EXCEPTION);
+                traceCallback.traceException(tag, message, e);
+            }
+        }
+    }
+
+    /**
+     * @param token
+     *            identifying an operation
+     * @return an identifier for the token which can be passed to the Android
+     *         Service
+     */
+    private synchronized String storeToken(IMqttToken token) {
+        tokenMap.put(tokenNumber, token);
+        return Integer.toString(tokenNumber++);
+    }
+
+    /**
+     * Get a token identified by a string, and remove it from our map
+     *
+     * @param data
+     * @return the token
+     */
+    private synchronized IMqttToken removeMqttToken(Bundle data) {
+
+        String activityToken = data.getString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN);
+        if (activityToken!=null){
+            int tokenNumber = Integer.parseInt(activityToken);
+            IMqttToken token = tokenMap.get(tokenNumber);
+            tokenMap.delete(tokenNumber);
+            return token;
+        }
+        return null;
+    }
+
+    /**
+     * Get a token identified by a string, and remove it from our map
+     *
+     * @param data
+     * @return the token
+     */
+    private synchronized IMqttToken getMqttToken(Bundle data) {
+        String activityToken = data
+                .getString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN);
+        return tokenMap.get(Integer.parseInt(activityToken));
+    }
+
+    /**
+     * Sets the DisconnectedBufferOptions for this client
+     * @param bufferOpts the DisconnectedBufferOptions
+     */
+    public void setBufferOpts(DisconnectedBufferOptions bufferOpts) {
+        mqttService.setBufferOpts(clientHandle, bufferOpts);
+    }
+
+    public int getBufferedMessageCount(){
+        return mqttService.getBufferedMessageCount(clientHandle);
+    }
+
+    public MqttMessage getBufferedMessage(int bufferIndex){
+        return mqttService.getBufferedMessage(clientHandle, bufferIndex);
+    }
+
+    public void deleteBufferedMessage(int bufferIndex){
+        mqttService.deleteBufferedMessage(clientHandle, bufferIndex);
+    }
+
+    @Override
+    public int getInFlightMessageCount() {
+        return 0;
+    }
+
+    /**
+     * Get the SSLSocketFactory using SSL key store and password
+     * <p>
+     * A convenience method, which will help user to create a SSLSocketFactory
+     * object
+     * </p>
+     *
+     * @param keyStore
+     *            the SSL key store which is generated by some SSL key tool,
+     *            such as keytool in Java JDK
+     * @param password
+     *            the password of the key store which is set when the key store
+     *            is generated
+     * @return SSLSocketFactory used to connect to the server with SSL
+     *         authentication
+     * @throws MqttSecurityException
+     *             if there was any error when getting the SSLSocketFactory
+     */
+    public SSLSocketFactory getSSLSocketFactory (InputStream keyStore, String password) throws MqttSecurityException {
+        try{
+            SSLContext ctx = null;
+            SSLSocketFactory sslSockFactory=null;
+            KeyStore ts;
+            ts = KeyStore.getInstance("BKS");
+            ts.load(keyStore, password.toCharArray());
+            TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
+            tmf.init(ts);
+            TrustManager[] tm = tmf.getTrustManagers();
+            ctx = SSLContext.getInstance("TLSv1");
+            ctx.init(null, tm, null);
+
+            sslSockFactory=ctx.getSocketFactory();
+            return sslSockFactory;
+
+        } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | KeyManagementException e) {
+            throw new MqttSecurityException(e);
+        }
+    }
+
+    @Override
+    public void disconnectForcibly() throws MqttException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void disconnectForcibly(long disconnectTimeout) throws MqttException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void disconnectForcibly(long quiesceTimeout, long disconnectTimeout)
+            throws MqttException {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Unregister receiver which receives intent from MqttService avoids
+     * IntentReceiver leaks.
+     */
+    public void unregisterResources(){
+        if(myContext != null && receiverRegistered){
+            synchronized (MqttAndroidClient.this) {
+                LocalBroadcastManager.getInstance(myContext).unregisterReceiver(this);
+                receiverRegistered = false;
+            }
+            if(bindedService){
+                try{
+                    myContext.unbindService(serviceConnection);
+                    bindedService = false;
+                }catch(IllegalArgumentException e){
+                    //Ignore unbind issue.
+                }
+            }
+        }
+    }
+
+    /**
+     * Register receiver to receiver intent from MqttService. Call this method
+     * when activity is hidden and become to show again.
+     *
+     * @param context
+     *            - Current activity context.
+     */
+    public void registerResources(Context context){
+        if(context != null){
+            this.myContext = context;
+            if(!receiverRegistered){
+                registerReceiver(this);
+            }
+        }
+    }
+}
+

+ 1132 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/MqttConnection.java

@@ -0,0 +1,1132 @@
+package com.somsakelect.android.mqtt;
+
+
+import android.app.Service;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.util.Log;
+
+import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions;
+import org.eclipse.paho.client.mqttv3.IMqttActionListener;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.IMqttMessageListener;
+import org.eclipse.paho.client.mqttv3.IMqttToken;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
+import org.eclipse.paho.client.mqttv3.MqttClientPersistence;
+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.MqttPersistenceException;
+import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * <p>
+ * MqttConnection holds a MqttAsyncClient {host,port,clientId} instance to perform
+ * MQTT operations to MQTT broker.
+ * </p>
+ * <p>
+ * Most of the major API here is intended to implement the most general forms of
+ * the methods in IMqttAsyncClient, with slight adjustments for the Android
+ * environment<br>
+ * These adjustments usually consist of adding two parameters to each method :-
+ * <ul>
+ * <li>invocationContext - a string passed from the application to identify the
+ * context of the operation (mainly included for support of the javascript API
+ * implementation)</li>
+ * <li>activityToken - a string passed from the Activity to relate back to a
+ * callback method or other context-specific data</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Operations are very much asynchronous, so success and failure are notified by
+ * packing the relevant data into Intent objects which are broadcast back to the
+ * Activity via the MqttService.callbackToActivity() method.
+ * </p>
+ */
+class MqttConnection implements MqttCallbackExtended {
+
+    // Strings for Intents etc..
+    private static final String TAG = "MqttConnection";
+    // Error status messages
+    private static final String NOT_CONNECTED = "not connected";
+
+    // fields for the connection definition
+    private String serverURI;
+    public String getServerURI() {
+        return serverURI;
+    }
+
+    public void setServerURI(String serverURI) {
+        this.serverURI = serverURI;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    private String clientId;
+    private MqttClientPersistence persistence = null;
+    private MqttConnectOptions connectOptions;
+
+    public MqttConnectOptions getConnectOptions() {
+        return connectOptions;
+    }
+
+    public void setConnectOptions(MqttConnectOptions connectOptions) {
+        this.connectOptions = connectOptions;
+    }
+
+    // Client handle, used for callbacks...
+    private String clientHandle;
+
+    public String getClientHandle() {
+        return clientHandle;
+    }
+
+    public void setClientHandle(String clientHandle) {
+        this.clientHandle = clientHandle;
+    }
+
+    //store connect ActivityToken for reconnect
+    private String reconnectActivityToken = null;
+
+    // our client object - instantiated on connect
+    private MqttAsyncClient myClient = null;
+
+    private AlarmPingSender alarmPingSender = null;
+
+    // our (parent) service object
+    private MqttService service = null;
+
+    private volatile boolean disconnected = true;
+    private boolean cleanSession = true;
+
+    // Indicate this connection is connecting or not.
+    // This variable uses to avoid reconnect multiple times.
+    private volatile boolean isConnecting = false;
+
+    // Saved sent messages and their corresponding Topics, activityTokens and
+    // invocationContexts, so we can handle "deliveryComplete" callbacks
+    // from the mqttClient
+    private Map<IMqttDeliveryToken, String /* Topic */> savedTopics = new HashMap<>();
+    private Map<IMqttDeliveryToken, MqttMessage> savedSentMessages = new HashMap<>();
+    private Map<IMqttDeliveryToken, String> savedActivityTokens = new HashMap<>();
+    private Map<IMqttDeliveryToken, String> savedInvocationContexts = new HashMap<>();
+
+    private PowerManager.WakeLock wakelock = null;
+    private String wakeLockTag = null;
+
+    private DisconnectedBufferOptions bufferOpts = null;
+
+    /**
+     * Constructor - create an MqttConnection to communicate with MQTT server
+     *
+     * @param service
+     *            our "parent" service - we make callbacks to it
+     * @param serverURI
+     *            the URI of the MQTT server to which we will connect
+     * @param clientId
+     *            the name by which we will identify ourselves to the MQTT
+     *            server
+     * @param persistence
+     *            the persistence class to use to store in-flight message. If
+     *            null then the default persistence mechanism is used
+     * @param clientHandle
+     *            the "handle" by which the activity will identify us
+     */
+    MqttConnection(MqttService service, String serverURI, String clientId,
+                   MqttClientPersistence persistence, String clientHandle) {
+        this.serverURI = serverURI;
+        this.service = service;
+        this.clientId = clientId;
+        this.persistence = persistence;
+        this.clientHandle = clientHandle;
+
+        StringBuilder stringBuilder = new StringBuilder(this.getClass().getCanonicalName());
+        stringBuilder.append(" ");
+        stringBuilder.append(clientId);
+        stringBuilder.append(" ");
+        stringBuilder.append("on host ");
+        stringBuilder.append(serverURI);
+        wakeLockTag = stringBuilder.toString();
+    }
+
+    // The major API implementation follows
+    /**
+     * Connect to the server specified when we were instantiated
+     *
+     * @param options
+     *            timeout, etc
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     */
+    public void connect(MqttConnectOptions options, String invocationContext,
+                        String activityToken) {
+
+        connectOptions = options;
+        reconnectActivityToken = activityToken;
+
+        if (options != null) {
+            cleanSession = options.isCleanSession();
+        }
+
+        if (connectOptions.isCleanSession()) { // if it's a clean session,
+            // discard old data
+            service.messageStore.clearArrivedMessages(clientHandle);
+        }
+
+        service.traceDebug(TAG, "Connecting {" + serverURI + "} as {" + clientId + "}");
+        final Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                activityToken);
+        resultBundle.putString(
+                MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT,
+                invocationContext);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.CONNECT_ACTION);
+
+
+        try {
+            if (persistence == null) {
+                // ask Android where we can put files
+                File myDir = service.getExternalFilesDir(TAG);
+
+                if (myDir == null) {
+                    // No external storage, use internal storage instead.
+                    myDir = service.getDir(TAG, Context.MODE_PRIVATE);
+
+                    if(myDir == null){
+                        //Shouldn't happen.
+                        resultBundle.putString(
+                                MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                                "Error! No external and internal storage available");
+                        resultBundle.putSerializable(
+                                MqttServiceConstants.CALLBACK_EXCEPTION, new MqttPersistenceException());
+                        service.callbackToActivity(clientHandle, Status.ERROR,
+                                resultBundle);
+                        return;
+                    }
+                }
+
+                // use that to setup MQTT client persistence storage
+                persistence = new MqttDefaultFilePersistence(
+                        myDir.getAbsolutePath());
+            }
+
+            IMqttActionListener listener = new MqttConnectionListener(
+                    resultBundle) {
+
+                @Override
+                public void onSuccess(IMqttToken asyncActionToken) {
+                    doAfterConnectSuccess(resultBundle);
+                    service.traceDebug(TAG, "connect success!");
+                }
+
+                @Override
+                public void onFailure(IMqttToken asyncActionToken,
+                                      Throwable exception) {
+                    resultBundle.putString(
+                            MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                            exception.getLocalizedMessage());
+                    resultBundle.putSerializable(
+                            MqttServiceConstants.CALLBACK_EXCEPTION, exception);
+                    service.traceError(TAG,
+                            "connect fail, call connect to reconnect.reason:"
+                                    + exception.getMessage());
+
+                    doAfterConnectFail(resultBundle);
+
+                }
+            };
+
+            if (myClient != null) {
+                if (isConnecting ) {
+                    service.traceDebug(TAG,
+                            "myClient != null and the client is connecting. Connect return directly.");
+                    service.traceDebug(TAG,"Connect return:isConnecting:"+isConnecting+".disconnected:"+disconnected);
+                }else if(!disconnected){
+                    service.traceDebug(TAG,"myClient != null and the client is connected and notify!");
+                    doAfterConnectSuccess(resultBundle);
+                }
+                else {
+                    service.traceDebug(TAG, "myClient != null and the client is not connected");
+                    service.traceDebug(TAG,"Do Real connect!");
+                    setConnectingState(true);
+                    myClient.connect(connectOptions, invocationContext, listener);
+                }
+            }
+
+            // if myClient is null, then create a new connection
+            else {
+                alarmPingSender = new AlarmPingSender(service);
+                myClient = new MqttAsyncClient(serverURI, clientId,
+                        persistence, alarmPingSender);
+                myClient.setCallback(this);
+
+                service.traceDebug(TAG,"Do Real connect!");
+                setConnectingState(true);
+                myClient.connect(connectOptions, invocationContext, listener);
+            }
+        } catch (Exception e) {
+            service.traceError(TAG, "Exception occurred attempting to connect: " + e.getMessage());
+            setConnectingState(false);
+            handleException(resultBundle, e);
+        }
+    }
+
+    private void doAfterConnectSuccess(final Bundle resultBundle) {
+        //since the device's cpu can go to sleep, acquire a wakelock and drop it later.
+        acquireWakeLock();
+        service.callbackToActivity(clientHandle, Status.OK, resultBundle);
+        deliverBacklog();
+        setConnectingState(false);
+        disconnected = false;
+        releaseWakeLock();
+    }
+
+    @Override
+    public void connectComplete(boolean reconnect, String serverURI) {
+        Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.CONNECT_EXTENDED_ACTION);
+        resultBundle.putBoolean(MqttServiceConstants.CALLBACK_RECONNECT, reconnect);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_SERVER_URI, serverURI);
+        service.callbackToActivity(clientHandle, Status.OK, resultBundle);
+    }
+
+    private void doAfterConnectFail(final Bundle resultBundle){
+        //
+        acquireWakeLock();
+        disconnected = true;
+        setConnectingState(false);
+        service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+        releaseWakeLock();
+    }
+
+    private void handleException(final Bundle resultBundle, Exception e) {
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                e.getLocalizedMessage());
+
+        resultBundle.putSerializable(MqttServiceConstants.CALLBACK_EXCEPTION, e);
+
+        service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+    }
+
+    /**
+     * Attempt to deliver any outstanding messages we've received but which the
+     * application hasn't acknowledged. If "cleanSession" was specified, we'll
+     * have already purged any such messages from our messageStore.
+     */
+    private void deliverBacklog() {
+        Iterator<MessageStore.StoredMessage> backlog = service.messageStore
+                .getAllArrivedMessages(clientHandle);
+        while (backlog.hasNext()) {
+            MessageStore.StoredMessage msgArrived = backlog.next();
+            Bundle resultBundle = messageToBundle(msgArrived.getMessageId(),
+                    msgArrived.getTopic(), msgArrived.getMessage());
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                    MqttServiceConstants.MESSAGE_ARRIVED_ACTION);
+            service.callbackToActivity(clientHandle, Status.OK, resultBundle);
+        }
+    }
+
+    /**
+     * Create a bundle containing all relevant data pertaining to a message
+     *
+     * @param messageId
+     *            the message's identifier in the messageStore, so that a
+     *            callback can be made to remove it once delivered
+     * @param topic
+     *            the topic on which the message was delivered
+     * @param message
+     *            the message itself
+     * @return the bundle
+     */
+    private Bundle messageToBundle(String messageId, String topic,
+                                   MqttMessage message) {
+        Bundle result = new Bundle();
+        result.putString(MqttServiceConstants.CALLBACK_MESSAGE_ID, messageId);
+        result.putString(MqttServiceConstants.CALLBACK_DESTINATION_NAME, topic);
+        result.putParcelable(MqttServiceConstants.CALLBACK_MESSAGE_PARCEL,
+                new ParcelableMqttMessage(message));
+        return result;
+    }
+
+    /**
+     * Close connection from the server
+     *
+     */
+    void close() {
+        service.traceDebug(TAG, "close()");
+        try {
+            if (myClient != null) {
+                myClient.close();
+            }
+        } catch (MqttException e) {
+            // Pass a new bundle, let handleException stores error messages.
+            handleException(new Bundle(), e);
+        }
+    }
+
+    /**
+     * Disconnect from the server
+     *
+     * @param quiesceTimeout
+     *            in milliseconds
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary string to be passed back to the activity
+     */
+    void disconnect(long quiesceTimeout, String invocationContext,
+                    String activityToken) {
+        service.traceDebug(TAG, "disconnect()");
+        disconnected = true;
+        final Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                activityToken);
+        resultBundle.putString(
+                MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT,
+                invocationContext);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.DISCONNECT_ACTION);
+        if ((myClient != null) && (myClient.isConnected())) {
+            IMqttActionListener listener = new MqttConnectionListener(
+                    resultBundle);
+            try {
+                myClient.disconnect(quiesceTimeout, invocationContext, listener);
+            } catch (Exception e) {
+                handleException(resultBundle, e);
+            }
+        } else {
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                    NOT_CONNECTED);
+            service.traceError(MqttServiceConstants.DISCONNECT_ACTION,
+                    NOT_CONNECTED);
+            service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+        }
+
+        if (connectOptions != null && connectOptions.isCleanSession()) {
+            // assume we'll clear the stored messages at this point
+            service.messageStore.clearArrivedMessages(clientHandle);
+        }
+
+        releaseWakeLock();
+    }
+
+    /**
+     * Disconnect from the server
+     *
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary string to be passed back to the activity
+     */
+    void disconnect(String invocationContext, String activityToken) {
+        service.traceDebug(TAG, "disconnect()");
+        disconnected = true;
+        final Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                activityToken);
+        resultBundle.putString(
+                MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT,
+                invocationContext);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.DISCONNECT_ACTION);
+        if ((myClient != null) && (myClient.isConnected())) {
+            IMqttActionListener listener = new MqttConnectionListener(
+                    resultBundle);
+            try {
+                myClient.disconnect(invocationContext, listener);
+            } catch (Exception e) {
+                handleException(resultBundle, e);
+            }
+        } else {
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                    NOT_CONNECTED);
+            service.traceError(MqttServiceConstants.DISCONNECT_ACTION,
+                    NOT_CONNECTED);
+            service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+        }
+
+        if (connectOptions != null && connectOptions.isCleanSession()) {
+            // assume we'll clear the stored messages at this point
+            service.messageStore.clearArrivedMessages(clientHandle);
+        }
+        releaseWakeLock();
+    }
+
+    /**
+     * @return true if we are connected to an MQTT server
+     */
+    public boolean isConnected() {
+        return myClient != null && myClient.isConnected();
+    }
+
+    /**
+     * Publish a message on a topic
+     *
+     * @param topic
+     *            the topic on which to publish - represented as a string, not
+     *            an MqttTopic object
+     * @param payload
+     *            the content of the message to publish
+     * @param qos
+     *            the quality of service requested
+     * @param retained
+     *            whether the MQTT server should retain this message
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary string to be passed back to the activity
+     * @return token for tracking the operation
+     */
+    public IMqttDeliveryToken publish(String topic, byte[] payload, int qos,
+                                      boolean retained, String invocationContext, String activityToken) {
+        final Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.SEND_ACTION);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                activityToken);
+        resultBundle.putString(
+                MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT,
+                invocationContext);
+
+        IMqttDeliveryToken sendToken = null;
+
+        if ((myClient != null) && (myClient.isConnected())) {
+            IMqttActionListener listener = new MqttConnectionListener(
+                    resultBundle);
+            try {
+                MqttMessage message = new MqttMessage(payload);
+                message.setQos(qos);
+                message.setRetained(retained);
+                sendToken = myClient.publish(topic, payload, qos, retained,
+                        invocationContext, listener);
+                storeSendDetails(topic, message, sendToken, invocationContext,
+                        activityToken);
+            } catch (Exception e) {
+                handleException(resultBundle, e);
+            }
+        } else {
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                    NOT_CONNECTED);
+            service.traceError(MqttServiceConstants.SEND_ACTION, NOT_CONNECTED);
+            service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+        }
+
+        return sendToken;
+    }
+
+    /**
+     * Publish a message on a topic
+     *
+     * @param topic
+     *            the topic on which to publish - represented as a string, not
+     *            an MqttTopic object
+     * @param message
+     *            the message to publish
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary string to be passed back to the activity
+     * @return token for tracking the operation
+     */
+    public IMqttDeliveryToken publish(String topic, MqttMessage message,
+                                      String invocationContext, String activityToken) {
+        final Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.SEND_ACTION);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                activityToken);
+        resultBundle.putString(
+                MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT,
+                invocationContext);
+
+        IMqttDeliveryToken sendToken = null;
+
+        if ((myClient != null) && (myClient.isConnected())) {
+            IMqttActionListener listener = new MqttConnectionListener(
+                    resultBundle);
+            try {
+                sendToken = myClient.publish(topic, message, invocationContext,
+                        listener);
+                storeSendDetails(topic, message, sendToken, invocationContext,
+                        activityToken);
+            } catch (Exception e) {
+                handleException(resultBundle, e);
+            }
+        } else if ((myClient !=null) && (this.bufferOpts != null) && (this.bufferOpts.isBufferEnabled())){
+            // Client is not connected, but buffer is enabled, so sending message
+            IMqttActionListener listener = new MqttConnectionListener(
+                    resultBundle);
+            try {
+                sendToken = myClient.publish(topic, message, invocationContext,
+                        listener);
+                storeSendDetails(topic, message, sendToken, invocationContext,
+                        activityToken);
+            } catch (Exception e) {
+                handleException(resultBundle, e);
+            }
+        }  else {
+            Log.i(TAG, "Client is not connected, so not sending message");
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                    NOT_CONNECTED);
+            service.traceError(MqttServiceConstants.SEND_ACTION, NOT_CONNECTED);
+            service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+        }
+        return sendToken;
+    }
+
+    /**
+     * Subscribe to a topic
+     *
+     * @param topic
+     *            a possibly wildcarded topic name
+     * @param qos
+     *            requested quality of service for the topic
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     */
+    public void subscribe(final String topic, final int qos,
+                          String invocationContext, String activityToken) {
+        service.traceDebug(TAG, "subscribe({" + topic + "}," + qos + ",{"
+                + invocationContext + "}, {" + activityToken + "}");
+        final Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.SUBSCRIBE_ACTION);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                activityToken);
+        resultBundle.putString(
+                MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT,
+                invocationContext);
+
+        if ((myClient != null) && (myClient.isConnected())) {
+            IMqttActionListener listener = new MqttConnectionListener(
+                    resultBundle);
+            try {
+                myClient.subscribe(topic, qos, invocationContext, listener);
+            } catch (Exception e) {
+                handleException(resultBundle, e);
+            }
+        } else {
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                    NOT_CONNECTED);
+            service.traceError("subscribe", NOT_CONNECTED);
+            service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+        }
+    }
+
+    /**
+     * Subscribe to one or more topics
+     *
+     * @param topic
+     *            a list of possibly wildcarded topic names
+     * @param qos
+     *            requested quality of service for each topic
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     */
+    public void subscribe(final String[] topic, final int[] qos,
+                          String invocationContext, String activityToken) {
+        service.traceDebug(TAG, "subscribe({" + Arrays.toString(topic) + "}," + Arrays.toString(qos) + ",{"
+                + invocationContext + "}, {" + activityToken + "}");
+        final Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.SUBSCRIBE_ACTION);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                activityToken);
+        resultBundle.putString(
+                MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT,
+                invocationContext);
+
+        if ((myClient != null) && (myClient.isConnected())) {
+            IMqttActionListener listener = new MqttConnectionListener(
+                    resultBundle);
+            try {
+                myClient.subscribe(topic, qos, invocationContext, listener);
+            } catch (Exception e) {
+                handleException(resultBundle, e);
+            }
+        } else {
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                    NOT_CONNECTED);
+            service.traceError("subscribe", NOT_CONNECTED);
+            service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+        }
+    }
+
+    public void subscribe(String[] topicFilters, int[] qos, String invocationContext, String activityToken, IMqttMessageListener[] messageListeners) {
+        service.traceDebug(TAG, "subscribe({" + Arrays.toString(topicFilters) + "}," + Arrays.toString(qos) + ",{"
+                + invocationContext + "}, {" + activityToken + "}");
+        final Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.SUBSCRIBE_ACTION);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext);
+        if((myClient != null) && (myClient.isConnected())){
+            IMqttActionListener listener = new MqttConnectionListener(resultBundle);
+            try {
+
+                myClient.subscribe(topicFilters, qos,messageListeners);
+            } catch (Exception e){
+                handleException(resultBundle, e);
+            }
+        } else {
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, NOT_CONNECTED);
+            service.traceError("subscribe", NOT_CONNECTED);
+            service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+        }
+    }
+
+    /**
+     * Unsubscribe from a topic
+     *
+     * @param topic
+     *            a possibly wildcarded topic name
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     */
+    void unsubscribe(final String topic, String invocationContext,
+                     String activityToken) {
+        service.traceDebug(TAG, "unsubscribe({" + topic + "},{"
+                + invocationContext + "}, {" + activityToken + "})");
+        final Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.UNSUBSCRIBE_ACTION);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                activityToken);
+        resultBundle.putString(
+                MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT,
+                invocationContext);
+        if ((myClient != null) && (myClient.isConnected())) {
+            IMqttActionListener listener = new MqttConnectionListener(
+                    resultBundle);
+            try {
+                myClient.unsubscribe(topic, invocationContext, listener);
+            } catch (Exception e) {
+                handleException(resultBundle, e);
+            }
+        } else {
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                    NOT_CONNECTED);
+
+            service.traceError("subscribe", NOT_CONNECTED);
+            service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+        }
+    }
+
+    /**
+     * Unsubscribe from one or more topics
+     *
+     * @param topic
+     *            a list of possibly wildcarded topic names
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     */
+    void unsubscribe(final String[] topic, String invocationContext,
+                     String activityToken) {
+        service.traceDebug(TAG, "unsubscribe({" + Arrays.toString(topic) + "},{"
+                + invocationContext + "}, {" + activityToken + "})");
+        final Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.UNSUBSCRIBE_ACTION);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                activityToken);
+        resultBundle.putString(
+                MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT,
+                invocationContext);
+        if ((myClient != null) && (myClient.isConnected())) {
+            IMqttActionListener listener = new MqttConnectionListener(
+                    resultBundle);
+            try {
+                myClient.unsubscribe(topic, invocationContext, listener);
+            } catch (Exception e) {
+                handleException(resultBundle, e);
+            }
+        } else {
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                    NOT_CONNECTED);
+
+            service.traceError("subscribe", NOT_CONNECTED);
+            service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+        }
+    }
+
+    /**
+     * Get tokens for all outstanding deliveries for a client
+     *
+     * @return an array (possibly empty) of tokens
+     */
+    public IMqttDeliveryToken[] getPendingDeliveryTokens() {
+        return myClient.getPendingDeliveryTokens();
+    }
+
+    // Implement MqttCallback
+    /**
+     * Callback for connectionLost
+     *
+     * @param why
+     *            the exeception causing the break in communications
+     */
+    @Override
+    public void connectionLost(Throwable why) {
+        service.traceDebug(TAG, "connectionLost(" + why.getMessage() + ")");
+        disconnected = true;
+        try {
+            if(!this.connectOptions.isAutomaticReconnect()) {
+                myClient.disconnect(null, new IMqttActionListener() {
+
+                    @Override
+                    public void onSuccess(IMqttToken asyncActionToken) {
+                        // No action
+                    }
+
+                    @Override
+                    public void onFailure(IMqttToken asyncActionToken,
+                                          Throwable exception) {
+                        // No action
+                    }
+                });
+            } else {
+                // Using the new Automatic reconnect functionality.
+                // We can't force a disconnection, but we can speed one up
+                alarmPingSender.schedule(100);
+
+            }
+        } catch (Exception e) {
+            // ignore it - we've done our best
+        }
+
+        Bundle resultBundle = new Bundle();
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.ON_CONNECTION_LOST_ACTION);
+        if (why != null) {
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                    why.getMessage());
+            if (why instanceof MqttException) {
+                resultBundle.putSerializable(
+                        MqttServiceConstants.CALLBACK_EXCEPTION, why);
+            }
+            resultBundle.putString(
+                    MqttServiceConstants.CALLBACK_EXCEPTION_STACK,
+                    Log.getStackTraceString(why));
+        }
+        service.callbackToActivity(clientHandle, Status.OK, resultBundle);
+        // client has lost connection no need for wake lock
+        releaseWakeLock();
+    }
+
+    /**
+     * Callback to indicate a message has been delivered (the exact meaning of
+     * "has been delivered" is dependent on the QOS value)
+     *
+     * @param messageToken
+     *            the messge token provided when the message was originally sent
+     */
+    @Override
+    public void deliveryComplete(IMqttDeliveryToken messageToken) {
+
+        service.traceDebug(TAG, "deliveryComplete(" + messageToken + ")");
+
+        MqttMessage message = savedSentMessages.remove(messageToken);
+        if (message != null) { // If I don't know about the message, it's
+            // irrelevant
+            String topic = savedTopics.remove(messageToken);
+            String activityToken = savedActivityTokens.remove(messageToken);
+            String invocationContext = savedInvocationContexts
+                    .remove(messageToken);
+
+            Bundle resultBundle = messageToBundle(null, topic, message);
+            if (activityToken != null) {
+                resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                        MqttServiceConstants.SEND_ACTION);
+                resultBundle.putString(
+                        MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                        activityToken);
+                resultBundle.putString(
+                        MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT,
+                        invocationContext);
+
+                service.callbackToActivity(clientHandle, Status.OK,
+                        resultBundle);
+            }
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                    MqttServiceConstants.MESSAGE_DELIVERED_ACTION);
+            service.callbackToActivity(clientHandle, Status.OK, resultBundle);
+        }
+
+        // this notification will have kept the connection alive but send the previously sechudled ping anyway
+    }
+
+    /**
+     * Callback when a message is received
+     *
+     * @param topic
+     *            the topic on which the message was received
+     * @param message
+     *            the message itself
+     */
+    @Override
+    public void messageArrived(String topic, MqttMessage message)
+            throws Exception {
+
+        service.traceDebug(TAG,
+                "messageArrived(" + topic + ",{" + message.toString() + "})");
+
+        String messageId = service.messageStore.storeArrived(clientHandle,
+                topic, message);
+
+        Bundle resultBundle = messageToBundle(messageId, topic, message);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                MqttServiceConstants.MESSAGE_ARRIVED_ACTION);
+        resultBundle.putString(MqttServiceConstants.CALLBACK_MESSAGE_ID,
+                messageId);
+        service.callbackToActivity(clientHandle, Status.OK, resultBundle);
+
+    }
+
+
+
+    /**
+     * Store details of sent messages so we can handle "deliveryComplete"
+     * callbacks from the mqttClient
+     *
+     * @param topic
+     * @param msg
+     * @param messageToken
+     * @param invocationContext
+     * @param activityToken
+     */
+    private void storeSendDetails(final String topic, final MqttMessage msg,
+                                  final IMqttDeliveryToken messageToken,
+                                  final String invocationContext, final String activityToken) {
+        savedTopics.put(messageToken, topic);
+        savedSentMessages.put(messageToken, msg);
+        savedActivityTokens.put(messageToken, activityToken);
+        savedInvocationContexts.put(messageToken, invocationContext);
+    }
+
+    /**
+     * Acquires a partial wake lock for this client
+     */
+    private void acquireWakeLock() {
+        if (wakelock == null) {
+            PowerManager pm = (PowerManager) service
+                    .getSystemService(Service.POWER_SERVICE);
+            wakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                    wakeLockTag);
+        }
+        wakelock.acquire();
+
+    }
+
+    /**
+     * Releases the currently held wake lock for this client
+     */
+    private void releaseWakeLock() {
+        if(wakelock != null && wakelock.isHeld()){
+            wakelock.release();
+        }
+    }
+
+
+
+    /**
+     * General-purpose IMqttActionListener for the Client context
+     * <p>
+     * Simply handles the basic success/failure cases for operations which don't
+     * return results
+     *
+     */
+    private class MqttConnectionListener implements IMqttActionListener {
+
+        private final Bundle resultBundle;
+
+        private MqttConnectionListener(Bundle resultBundle) {
+            this.resultBundle = resultBundle;
+        }
+
+        @Override
+        public void onSuccess(IMqttToken asyncActionToken) {
+            service.callbackToActivity(clientHandle, Status.OK, resultBundle);
+        }
+
+        @Override
+        public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                    exception.getLocalizedMessage());
+
+            resultBundle.putSerializable(
+                    MqttServiceConstants.CALLBACK_EXCEPTION, exception);
+
+            service.callbackToActivity(clientHandle, Status.ERROR, resultBundle);
+        }
+    }
+
+    /**
+     * Receive notification that we are offline<br>
+     * if cleanSession is true, we need to regard this as a disconnection
+     */
+    void offline() {
+
+        if (!disconnected && !cleanSession) {
+            Exception e = new Exception("Android offline");
+            connectionLost(e);
+        }
+    }
+
+    /**
+     * Reconnect<br>
+     * Only appropriate if cleanSession is false and we were connected.
+     * Declare as synchronized to avoid multiple calls to this method to send connect
+     * multiple times
+     */
+    synchronized void reconnect() {
+
+        if (myClient == null) {
+            service.traceError(TAG,"Reconnect myClient = null. Will not do reconnect");
+            return;
+        }
+
+        if (isConnecting) {
+            service.traceDebug(TAG, "The client is connecting. Reconnect return directly.");
+            return ;
+        }
+
+        if(!service.isOnline()){
+            service.traceDebug(TAG,
+                    "The network is not reachable. Will not do reconnect");
+            return;
+        }
+
+        if(connectOptions.isAutomaticReconnect()){
+            //The Automatic reconnect functionality is enabled here
+            Log.i(TAG, "Requesting Automatic reconnect using New Java AC");
+            final Bundle resultBundle = new Bundle();
+            resultBundle.putString(
+                    MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                    reconnectActivityToken);
+            resultBundle.putString(
+                    MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, null);
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                    MqttServiceConstants.CONNECT_ACTION);
+            try {
+                myClient.reconnect();
+            } catch (MqttException ex){
+                Log.e(TAG, "Exception occurred attempting to reconnect: " + ex.getMessage());
+                setConnectingState(false);
+                handleException(resultBundle, ex);
+            }
+        } else if (disconnected && !cleanSession) {
+            // use the activityToke the same with action connect
+            service.traceDebug(TAG,"Do Real Reconnect!");
+            final Bundle resultBundle = new Bundle();
+            resultBundle.putString(
+                    MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN,
+                    reconnectActivityToken);
+            resultBundle.putString(
+                    MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, null);
+            resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION,
+                    MqttServiceConstants.CONNECT_ACTION);
+
+            try {
+
+                IMqttActionListener listener = new MqttConnectionListener(resultBundle) {
+                    @Override
+                    public void onSuccess(IMqttToken asyncActionToken) {
+                        // since the device's cpu can go to sleep, acquire a
+                        // wakelock and drop it later.
+                        service.traceDebug(TAG,"Reconnect Success!");
+                        service.traceDebug(TAG,"DeliverBacklog when reconnect.");
+                        doAfterConnectSuccess(resultBundle);
+                    }
+
+                    @Override
+                    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+                        resultBundle.putString(
+                                MqttServiceConstants.CALLBACK_ERROR_MESSAGE,
+                                exception.getLocalizedMessage());
+                        resultBundle.putSerializable(
+                                MqttServiceConstants.CALLBACK_EXCEPTION,
+                                exception);
+                        service.callbackToActivity(clientHandle, Status.ERROR,
+                                resultBundle);
+
+                        doAfterConnectFail(resultBundle);
+
+                    }
+                };
+
+                myClient.connect(connectOptions, null, listener);
+                setConnectingState(true);
+            } catch (MqttException e) {
+                service.traceError(TAG, "Cannot reconnect to remote server." + e.getMessage());
+                setConnectingState(false);
+                handleException(resultBundle, e);
+            } catch (Exception e){
+				/*  TODO: Added Due to: https://github.com/eclipse/paho.mqtt.android/issues/101
+				    For some reason in a small number of cases, myClient is null here and so
+				    a NullPointer Exception is thrown. This is a workaround to pass the exception
+				    up to the application. myClient should not be null so more investigation is
+				    required.
+				*/
+                service.traceError(TAG, "Cannot reconnect to remote server." + e.getMessage());
+                setConnectingState(false);
+                MqttException newEx = new MqttException(MqttException.REASON_CODE_UNEXPECTED_ERROR, e.getCause());
+                handleException(resultBundle, newEx);
+            }
+        }
+    }
+
+    /**
+     *
+     * @param isConnecting
+     */
+    private synchronized void setConnectingState(boolean isConnecting){
+        this.isConnecting = isConnecting;
+    }
+
+    /**
+     * Sets the DisconnectedBufferOptions for this client
+     * @param bufferOpts
+     */
+    public void setBufferOpts(DisconnectedBufferOptions bufferOpts) {
+        this.bufferOpts = bufferOpts;
+        myClient.setBufferOpts(bufferOpts);
+    }
+
+    public int getBufferedMessageCount(){
+        return myClient.getBufferedMessageCount();
+    }
+
+    public MqttMessage getBufferedMessage(int bufferIndex){
+        return myClient.getBufferedMessage(bufferIndex);
+    }
+
+    public void deleteBufferedMessage(int bufferIndex){
+        myClient.deleteBufferedMessage(bufferIndex);
+    }
+}
+

+ 44 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/MqttDeliveryTokenAndroid.java

@@ -0,0 +1,44 @@
+package com.somsakelect.android.mqtt;
+
+
+import org.eclipse.paho.client.mqttv3.IMqttActionListener;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+
+/**
+ * <p>
+ * Implementation of the IMqttDeliveryToken interface for use from within the
+ * MqttAndroidClient implementation
+ */
+class MqttDeliveryTokenAndroid extends MqttTokenAndroid
+        implements IMqttDeliveryToken {
+
+    // The message which is being tracked by this token
+    private MqttMessage message;
+
+    MqttDeliveryTokenAndroid(MqttAndroidClient client,
+                             Object userContext, IMqttActionListener listener, MqttMessage message) {
+        super(client, userContext, listener);
+        this.message = message;
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttDeliveryToken#getMessage()
+     */
+    @Override
+    public MqttMessage getMessage() throws MqttException {
+        return message;
+    }
+
+    void setMessage(MqttMessage message) {
+        this.message = message;
+    }
+
+    void notifyDelivery(MqttMessage delivered) {
+        message = delivered;
+        super.notifyComplete();
+    }
+
+}
+

+ 900 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/MqttService.java

@@ -0,0 +1,900 @@
+package com.somsakelect.android.mqtt;
+
+
+import android.annotation.SuppressLint;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.PowerManager;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.IMqttMessageListener;
+import org.eclipse.paho.client.mqttv3.MqttClientPersistence;
+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.MqttPersistenceException;
+import org.eclipse.paho.client.mqttv3.MqttSecurityException;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * <p>
+ * The android service which interfaces with an MQTT client implementation
+ * </p>
+ * <p>
+ * The main API of MqttService is intended to pretty much mirror the
+ * IMqttAsyncClient with appropriate adjustments for the Android environment.<br>
+ * These adjustments usually consist of adding two parameters to each method :-
+ * </p>
+ * <ul>
+ * <li>invocationContext - a string passed from the application to identify the
+ * context of the operation (mainly included for support of the javascript API
+ * implementation)</li>
+ * <li>activityToken - a string passed from the Activity to relate back to a
+ * callback method or other context-specific data</li>
+ * </ul>
+ * <p>
+ * To support multiple client connections, the bulk of the MQTT work is
+ * delegated to MqttConnection objects. These are identified by "client
+ * handle" strings, which is how the Activity, and the higher-level APIs refer
+ * to them.
+ * </p>
+ * <p>
+ * Activities using this service are expected to start it and bind to it using
+ * the BIND_AUTO_CREATE flag. The life cycle of this service is based on this
+ * approach.
+ * </p>
+ * <p>
+ * Operations are highly asynchronous - in most cases results are returned to
+ * the Activity by broadcasting one (or occasionally more) appropriate Intents,
+ * which the Activity is expected to register a listener for.<br>
+ * The Intents have an Action of
+ * {@link MqttServiceConstants#CALLBACK_TO_ACTIVITY
+ * MqttServiceConstants.CALLBACK_TO_ACTIVITY} which allows the Activity to
+ * register a listener with an appropriate IntentFilter.<br>
+ * Further data is provided by "Extra Data" in the Intent, as follows :-
+ * </p>
+ * <table border="1" summary="">
+ * <tr>
+ * <th align="left">Name</th>
+ * <th align="left">Data Type</th>
+ * <th align="left">Value</th>
+ * <th align="left">Operations used for</th>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#CALLBACK_CLIENT_HANDLE
+ * MqttServiceConstants.CALLBACK_CLIENT_HANDLE}</td>
+ * <td align="left" valign="top">String</td>
+ * <td align="left" valign="top">The clientHandle identifying the client which
+ * initiated this operation</td>
+ * <td align="left" valign="top">All operations</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">{@link MqttServiceConstants#CALLBACK_STATUS
+ * MqttServiceConstants.CALLBACK_STATUS}</td>
+ * <td align="left" valign="top">Serializable</td>
+ * <td align="left" valign="top">An {@link Status} value indicating success or
+ * otherwise of the operation</td>
+ * <td align="left" valign="top">All operations</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#CALLBACK_ACTIVITY_TOKEN
+ * MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN}</td>
+ * <td align="left" valign="top">String</td>
+ * <td align="left" valign="top">the activityToken passed into the operation</td>
+ * <td align="left" valign="top">All operations</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#CALLBACK_INVOCATION_CONTEXT
+ * MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT}</td>
+ * <td align="left" valign="top">String</td>
+ * <td align="left" valign="top">the invocationContext passed into the operation
+ * </td>
+ * <td align="left" valign="top">All operations</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">{@link MqttServiceConstants#CALLBACK_ACTION
+ * MqttServiceConstants.CALLBACK_ACTION}</td>
+ * <td align="left" valign="top">String</td>
+ * <td align="left" valign="top">one of
+ * <table summary="">
+ * <tr>
+ * <td align="left" valign="top"> {@link MqttServiceConstants#SEND_ACTION
+ * MqttServiceConstants.SEND_ACTION}</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#UNSUBSCRIBE_ACTION
+ * MqttServiceConstants.UNSUBSCRIBE_ACTION}</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top"> {@link MqttServiceConstants#SUBSCRIBE_ACTION
+ * MqttServiceConstants.SUBSCRIBE_ACTION}</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top"> {@link MqttServiceConstants#DISCONNECT_ACTION
+ * MqttServiceConstants.DISCONNECT_ACTION}</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top"> {@link MqttServiceConstants#CONNECT_ACTION
+ * MqttServiceConstants.CONNECT_ACTION}</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#MESSAGE_ARRIVED_ACTION
+ * MqttServiceConstants.MESSAGE_ARRIVED_ACTION}</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#MESSAGE_DELIVERED_ACTION
+ * MqttServiceConstants.MESSAGE_DELIVERED_ACTION}</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#ON_CONNECTION_LOST_ACTION
+ * MqttServiceConstants.ON_CONNECTION_LOST_ACTION}</td>
+ * </tr>
+ * </table>
+ * </td>
+ * <td align="left" valign="top">All operations</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#CALLBACK_ERROR_MESSAGE
+ * MqttServiceConstants.CALLBACK_ERROR_MESSAGE}
+ * <td align="left" valign="top">String</td>
+ * <td align="left" valign="top">A suitable error message (taken from the
+ * relevant exception where possible)</td>
+ * <td align="left" valign="top">All failing operations</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#CALLBACK_ERROR_NUMBER
+ * MqttServiceConstants.CALLBACK_ERROR_NUMBER}
+ * <td align="left" valign="top">int</td>
+ * <td align="left" valign="top">A suitable error code (taken from the relevant
+ * exception where possible)</td>
+ * <td align="left" valign="top">All failing operations</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#CALLBACK_EXCEPTION_STACK
+ * MqttServiceConstants.CALLBACK_EXCEPTION_STACK}</td>
+ * <td align="left" valign="top">String</td>
+ * <td align="left" valign="top">The stacktrace of the failing call</td>
+ * <td align="left" valign="top">The Connection Lost event</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#CALLBACK_MESSAGE_ID
+ * MqttServiceConstants.CALLBACK_MESSAGE_ID}</td>
+ * <td align="left" valign="top">String</td>
+ * <td align="left" valign="top">The identifier for the message in the message
+ * store, used by the Activity to acknowledge the arrival of the message, so
+ * that the service may remove it from the store</td>
+ * <td align="left" valign="top">The Message Arrived event</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#CALLBACK_DESTINATION_NAME
+ * MqttServiceConstants.CALLBACK_DESTINATION_NAME}
+ * <td align="left" valign="top">String</td>
+ * <td align="left" valign="top">The topic on which the message was received</td>
+ * <td align="left" valign="top">The Message Arrived event</td>
+ * </tr>
+ * <tr>
+ * <td align="left" valign="top">
+ * {@link MqttServiceConstants#CALLBACK_MESSAGE_PARCEL
+ * MqttServiceConstants.CALLBACK_MESSAGE_PARCEL}</td>
+ * <td align="left" valign="top">Parcelable</td>
+ * <td align="left" valign="top">The new message encapsulated in Android
+ * Parcelable format as a {@link ParcelableMqttMessage}</td>
+ * <td align="left" valign="top">The Message Arrived event</td>
+ * </tr>
+ * </table>
+ */
+@SuppressLint("Registered")
+public class MqttService extends Service implements MqttTraceHandler {
+
+    // Identifier for Intents, log messages, etc..
+    static final String TAG = "MqttService";
+
+    // callback id for making trace callbacks to the Activity
+    // needs to be set by the activity as appropriate
+    private String traceCallbackId;
+    // state of tracing
+    private boolean traceEnabled = false;
+
+    // somewhere to persist received messages until we're sure
+    // that they've reached the application
+    MessageStore messageStore;
+
+    // An intent receiver to deal with changes in network connectivity
+    private NetworkConnectionIntentReceiver networkConnectionMonitor;
+
+    //a receiver to recognise when the user changes the "background data" preference
+    // and a flag to track that preference
+    // Only really relevant below android version ICE_CREAM_SANDWICH - see
+    // android docs
+    private BackgroundDataPreferenceReceiver backgroundDataPreferenceMonitor;
+    private volatile boolean backgroundDataEnabled = true;
+
+    // a way to pass ourself back to the activity
+    private MqttServiceBinder mqttServiceBinder;
+
+    // mapping from client handle strings to actual client connections.
+    private Map<String/* clientHandle */, MqttConnection/* client */> connections = new ConcurrentHashMap<>();
+
+    public MqttService() {
+        super();
+    }
+
+    /**
+     * pass data back to the Activity, by building a suitable Intent object and
+     * broadcasting it
+     *
+     * @param clientHandle
+     *            source of the data
+     * @param status
+     *            OK or Error
+     * @param dataBundle
+     *            the data to be passed
+     */
+    void callbackToActivity(String clientHandle, Status status,
+                            Bundle dataBundle) {
+        // Don't call traceDebug, as it will try to callbackToActivity leading
+        // to recursion.
+        Intent callbackIntent = new Intent(
+                MqttServiceConstants.CALLBACK_TO_ACTIVITY);
+        if (clientHandle != null) {
+            callbackIntent.putExtra(
+                    MqttServiceConstants.CALLBACK_CLIENT_HANDLE, clientHandle);
+        }
+        callbackIntent.putExtra(MqttServiceConstants.CALLBACK_STATUS, status);
+        if (dataBundle != null) {
+            callbackIntent.putExtras(dataBundle);
+        }
+        LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
+    }
+
+    // The major API implementation follows :-
+
+    /**
+     * Get an MqttConnection object to represent a connection to a server
+     *
+     * @param serverURI specifies the protocol, host name and port to be used to connect to an MQTT server
+     * @param clientId specifies the name by which this connection should be identified to the server
+     * @param contextId specifies the app conext info to make a difference between apps
+     * @param persistence specifies the persistence layer to be used with this client
+     * @return a string to be used by the Activity as a "handle" for this
+     *         MqttConnection
+     */
+    public String getClient(String serverURI, String clientId, String contextId, MqttClientPersistence persistence) {
+        String clientHandle = serverURI + ":" + clientId+":"+contextId;
+        if (!connections.containsKey(clientHandle)) {
+            MqttConnection client = new MqttConnection(this, serverURI,
+                    clientId, persistence, clientHandle);
+            connections.put(clientHandle, client);
+        }
+        return clientHandle;
+    }
+
+    /**
+     * Connect to the MQTT server specified by a particular client
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection to use
+     * @param connectOptions
+     *            the MQTT connection options to be used
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     * @throws MqttSecurityException thrown if there is a security exception
+     * @throws MqttException thrown for all other MqttExceptions
+     */
+    public void connect(String clientHandle, MqttConnectOptions connectOptions,
+                        String invocationContext, String activityToken)
+            throws MqttSecurityException, MqttException {
+        MqttConnection client = getConnection(clientHandle);
+        client.connect(connectOptions, null, activityToken);
+
+    }
+
+    /**
+     * Request all clients to reconnect if appropriate
+     */
+    void reconnect() {
+        traceDebug(TAG, "Reconnect to server, client size=" + connections.size());
+        for (MqttConnection client : connections.values()) {
+            traceDebug("Reconnect Client:",
+                    client.getClientId() + '/' + client.getServerURI());
+            if(this.isOnline()){
+                client.reconnect();
+            }
+        }
+    }
+
+    /**
+     * Close connection from a particular client
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection to use
+     */
+    public void close(String clientHandle) {
+        MqttConnection client = getConnection(clientHandle);
+        client.close();
+    }
+
+    /**
+     * Disconnect from the server
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection to use
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     */
+    public void disconnect(String clientHandle, String invocationContext,
+                           String activityToken) {
+        MqttConnection client = getConnection(clientHandle);
+        client.disconnect(invocationContext, activityToken);
+        connections.remove(clientHandle);
+
+
+        // the activity has finished using us, so we can stop the service
+        // the activities are bound with BIND_AUTO_CREATE, so the service will
+        // remain around until the last activity disconnects
+        stopSelf();
+    }
+
+    /**
+     * Disconnect from the server
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection to use
+     * @param quiesceTimeout
+     *            in milliseconds
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     */
+    public void disconnect(String clientHandle, long quiesceTimeout,
+                           String invocationContext, String activityToken) {
+        MqttConnection client = getConnection(clientHandle);
+        client.disconnect(quiesceTimeout, invocationContext, activityToken);
+        connections.remove(clientHandle);
+
+        // the activity has finished using us, so we can stop the service
+        // the activities are bound with BIND_AUTO_CREATE, so the service will
+        // remain around until the last activity disconnects
+        stopSelf();
+    }
+
+    /**
+     * Get the status of a specific client
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection to use
+     * @return true if the specified client is connected to an MQTT server
+     */
+    public boolean isConnected(String clientHandle) {
+        MqttConnection client = getConnection(clientHandle);
+        return client.isConnected();
+    }
+
+    /**
+     * Publish a message to a topic
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection to use
+     * @param topic
+     *            the topic to which to publish
+     * @param payload
+     *            the content of the message to publish
+     * @param qos
+     *            the quality of service requested
+     * @param retained
+     *            whether the MQTT server should retain this message
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     * @throws MqttPersistenceException when a problem occurs storing the message
+     * @throws MqttException if there was an error publishing the message
+     * @return token for tracking the operation
+     */
+    public IMqttDeliveryToken publish(String clientHandle, String topic,
+                                      byte[] payload, int qos, boolean retained,
+                                      String invocationContext, String activityToken)
+            throws MqttPersistenceException, MqttException {
+        MqttConnection client = getConnection(clientHandle);
+        return client.publish(topic, payload, qos, retained, invocationContext,
+                activityToken);
+    }
+
+    /**
+     * Publish a message to a topic
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection to use
+     * @param topic
+     *            the topic to which to publish
+     * @param message
+     *            the message to publish
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     * @throws MqttPersistenceException when a problem occurs storing the message
+     * @throws MqttException if there was an error publishing the message
+     * @return token for tracking the operation
+     */
+    public IMqttDeliveryToken publish(String clientHandle, String topic,
+                                      MqttMessage message, String invocationContext, String activityToken)
+            throws MqttPersistenceException, MqttException {
+        MqttConnection client = getConnection(clientHandle);
+        return client.publish(topic, message, invocationContext, activityToken);
+    }
+
+    /**
+     * Subscribe to a topic
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection to use
+     * @param topic
+     *            a possibly wildcarded topic name
+     * @param qos
+     *            requested quality of service for the topic
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     */
+    public void subscribe(String clientHandle, String topic, int qos,
+                          String invocationContext, String activityToken) {
+        MqttConnection client = getConnection(clientHandle);
+        client.subscribe(topic, qos, invocationContext, activityToken);
+    }
+
+    /**
+     * Subscribe to one or more topics
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection to use
+     * @param topic
+     *            a list of possibly wildcarded topic names
+     * @param qos
+     *            requested quality of service for each topic
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     */
+    public void subscribe(String clientHandle, String[] topic, int[] qos,
+                          String invocationContext, String activityToken) {
+        MqttConnection client = getConnection(clientHandle);
+        client.subscribe(topic, qos, invocationContext, activityToken);
+    }
+
+    /**
+     * Subscribe using topic filters
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection to use
+     * @param topicFilters
+     *            a list of possibly wildcarded topicfilters
+     * @param qos
+     *            requested quality of service for each topic
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     * @param messageListeners a callback to handle incoming messages
+     */
+    public void subscribe(String clientHandle, String[] topicFilters, int[] qos,
+                          String invocationContext, String activityToken,
+                          IMqttMessageListener[] messageListeners){
+        MqttConnection client = getConnection(clientHandle);
+        client.subscribe(topicFilters, qos, invocationContext, activityToken, messageListeners);
+    }
+
+    /**
+     * Unsubscribe from a topic
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection
+     * @param topic
+     *            a possibly wildcarded topic name
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     */
+    public void unsubscribe(String clientHandle, final String topic,
+                            String invocationContext, String activityToken) {
+        MqttConnection client = getConnection(clientHandle);
+        client.unsubscribe(topic, invocationContext, activityToken);
+    }
+
+    /**
+     * Unsubscribe from one or more topics
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection
+     * @param topic
+     *            a list of possibly wildcarded topic names
+     * @param invocationContext
+     *            arbitrary data to be passed back to the application
+     * @param activityToken
+     *            arbitrary identifier to be passed back to the Activity
+     */
+    public void unsubscribe(String clientHandle, final String[] topic,
+                            String invocationContext, String activityToken) {
+        MqttConnection client = getConnection(clientHandle);
+        client.unsubscribe(topic, invocationContext, activityToken);
+    }
+
+    /**
+     * Get tokens for all outstanding deliveries for a client
+     *
+     * @param clientHandle
+     *            identifies the MqttConnection
+     * @return an array (possibly empty) of tokens
+     */
+    public IMqttDeliveryToken[] getPendingDeliveryTokens(String clientHandle) {
+        MqttConnection client = getConnection(clientHandle);
+        return client.getPendingDeliveryTokens();
+    }
+
+    /**
+     * Get the MqttConnection identified by this client handle
+     *
+     * @param clientHandle identifies the MqttConnection
+     * @return the MqttConnection identified by this handle
+     */
+    private MqttConnection getConnection(String clientHandle) {
+        MqttConnection client = connections.get(clientHandle);
+        if (client == null) {
+            throw new IllegalArgumentException("Invalid ClientHandle");
+        }
+        return client;
+    }
+
+    /**
+     * Called by the Activity when a message has been passed back to the
+     * application
+     *
+     * @param clientHandle identifier for the client which received the message
+     * @param id identifier for the MQTT message
+     * @return {@link Status}
+     */
+    public Status acknowledgeMessageArrival(String clientHandle, String id) {
+        if (messageStore.discardArrived(clientHandle, id)) {
+            return Status.OK;
+        }
+        else {
+            return Status.ERROR;
+        }
+    }
+
+    // Extend Service
+
+    /**
+     * @see android.app.Service#onCreate()
+     */
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        // create a binder that will let the Activity UI send
+        // commands to the Service
+        mqttServiceBinder = new MqttServiceBinder(this);
+
+        // create somewhere to buffer received messages until
+        // we know that they have been passed to the application
+        messageStore = new DatabaseMessageStore(this, this);
+    }
+
+
+
+    /**
+     * @see android.app.Service#onDestroy()
+     */
+    @Override
+    public void onDestroy() {
+        // disconnect immediately
+        for (MqttConnection client : connections.values()) {
+            client.disconnect(null, null);
+        }
+
+        // clear down
+        if (mqttServiceBinder != null) {
+            mqttServiceBinder = null;
+        }
+
+        unregisterBroadcastReceivers();
+
+        if (this.messageStore !=null )
+            this.messageStore.close();
+
+        super.onDestroy();
+    }
+
+    /**
+     * @see android.app.Service#onBind(Intent)
+     */
+    @Override
+    public IBinder onBind(Intent intent) {
+        // What we pass back to the Activity on binding -
+        // a reference to ourself, and the activityToken
+        // we were given when started
+        String activityToken = intent
+                .getStringExtra(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN);
+        mqttServiceBinder.setActivityToken(activityToken);
+        return mqttServiceBinder;
+    }
+
+    /**
+     * @see android.app.Service#onStartCommand(Intent,int,int)
+     */
+    @Override
+    public int onStartCommand(final Intent intent, int flags, final int startId) {
+        // run till explicitly stopped, restart when
+        // process restarted
+        registerBroadcastReceivers();
+
+        return START_STICKY;
+    }
+
+    /**
+     * Identify the callbackId to be passed when making tracing calls back into
+     * the Activity
+     *
+     * @param traceCallbackId identifier to the callback into the Activity
+     */
+    public void setTraceCallbackId(String traceCallbackId) {
+        this.traceCallbackId = traceCallbackId;
+    }
+
+    /**
+     * Turn tracing on and off
+     *
+     * @param traceEnabled set <code>true</code> to turn on tracing, <code>false</code> to turn off tracing
+     */
+    public void setTraceEnabled(boolean traceEnabled) {
+        this.traceEnabled = traceEnabled;
+    }
+
+    /**
+     * Check whether trace is on or off.
+     *
+     * @return the state of trace
+     */
+    public boolean isTraceEnabled(){
+        return this.traceEnabled;
+    }
+
+    /**
+     * Trace debugging information
+     *
+     * @param tag
+     *            identifier for the source of the trace
+     * @param message
+     *            the text to be traced
+     */
+    @Override
+    public void traceDebug(String tag, String message) {
+        traceCallback(MqttServiceConstants.TRACE_DEBUG, tag, message);
+    }
+
+    /**
+     * Trace error information
+     *
+     * @param tag
+     *            identifier for the source of the trace
+     * @param message
+     *            the text to be traced
+     */
+    @Override
+    public void traceError(String tag, String message) {
+        traceCallback(MqttServiceConstants.TRACE_ERROR, tag, message);
+    }
+
+    private void traceCallback(String severity, String tag, String message) {
+        if ((traceCallbackId != null) && (traceEnabled)) {
+            Bundle dataBundle = new Bundle();
+            dataBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.TRACE_ACTION);
+            dataBundle.putString(MqttServiceConstants.CALLBACK_TRACE_SEVERITY, severity);
+            dataBundle.putString(MqttServiceConstants.CALLBACK_TRACE_TAG, tag);
+            //dataBundle.putString(MqttServiceConstants.CALLBACK_TRACE_ID, traceCallbackId);
+            dataBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, message);
+            callbackToActivity(traceCallbackId, Status.ERROR, dataBundle);
+        }
+    }
+
+    /**
+     * trace exceptions
+     *
+     * @param tag
+     *            identifier for the source of the trace
+     * @param message
+     *            the text to be traced
+     * @param e
+     *            the exception
+     */
+    @Override
+    public void traceException(String tag, String message, Exception e) {
+        if (traceCallbackId != null) {
+            Bundle dataBundle = new Bundle();
+            dataBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.TRACE_ACTION);
+            dataBundle.putString(MqttServiceConstants.CALLBACK_TRACE_SEVERITY, MqttServiceConstants.TRACE_EXCEPTION);
+            dataBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,  message);
+            dataBundle.putSerializable(MqttServiceConstants.CALLBACK_EXCEPTION, e); //TODO: Check
+            dataBundle.putString(MqttServiceConstants.CALLBACK_TRACE_TAG, tag);
+            //dataBundle.putString(MqttServiceConstants.CALLBACK_TRACE_ID, traceCallbackId);
+            callbackToActivity(traceCallbackId, Status.ERROR, dataBundle);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private void registerBroadcastReceivers() {
+        if (networkConnectionMonitor == null) {
+            networkConnectionMonitor = new NetworkConnectionIntentReceiver();
+            registerReceiver(networkConnectionMonitor, new IntentFilter(
+                    ConnectivityManager.CONNECTIVITY_ACTION));
+        }
+
+        if (Build.VERSION.SDK_INT < 14 /**Build.VERSION_CODES.ICE_CREAM_SANDWICH**/) {
+            // Support the old system for background data preferences
+            ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
+            backgroundDataEnabled = cm.getBackgroundDataSetting();
+            if (backgroundDataPreferenceMonitor == null) {
+                backgroundDataPreferenceMonitor = new BackgroundDataPreferenceReceiver();
+                registerReceiver(
+                        backgroundDataPreferenceMonitor,
+                        new IntentFilter(
+                                ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED));
+            }
+        }
+    }
+
+    private void unregisterBroadcastReceivers(){
+        if(networkConnectionMonitor != null){
+            unregisterReceiver(networkConnectionMonitor);
+            networkConnectionMonitor = null;
+        }
+
+        if (Build.VERSION.SDK_INT < 14 /**Build.VERSION_CODES.ICE_CREAM_SANDWICH**/) {
+            if(backgroundDataPreferenceMonitor != null){
+                unregisterReceiver(backgroundDataPreferenceMonitor);
+            }
+        }
+    }
+
+    /*
+     * Called in response to a change in network connection - after losing a
+     * connection to the server, this allows us to wait until we have a usable
+     * data connection again
+     */
+    private class NetworkConnectionIntentReceiver extends BroadcastReceiver {
+
+        @Override
+        @SuppressLint("Wakelock")
+        public void onReceive(Context context, Intent intent) {
+            traceDebug(TAG, "Internal network status receive.");
+            // we protect against the phone switching off
+            // by requesting a wake lock - we request the minimum possible wake
+            // lock - just enough to keep the CPU running until we've finished
+            PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
+            PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, String.valueOf(1357));
+            wl.acquire(10*60*1000L /*10 minutes*/);
+            traceDebug(TAG,"Reconnect for Network recovery.");
+            if (isOnline()) {
+                traceDebug(TAG,"Online,reconnect.");
+                // we have an internet connection - have another try at
+                // connecting
+                reconnect();
+            } else {
+                notifyClientsOffline();
+            }
+
+            wl.release();
+        }
+    }
+
+    /**
+     * @return whether the android service can be regarded as online
+     */
+    public boolean isOnline() {
+        ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
+        NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+        //noinspection RedundantIfStatement
+        if (networkInfo != null
+                && networkInfo.isAvailable()
+                && networkInfo.isConnected()
+                && backgroundDataEnabled) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Notify clients we're offline
+     */
+    private void notifyClientsOffline() {
+        for (MqttConnection connection : connections.values()) {
+            connection.offline();
+        }
+    }
+
+    /**
+     * Detect changes of the Allow Background Data setting - only used below
+     * ICE_CREAM_SANDWICH
+     */
+    private class BackgroundDataPreferenceReceiver extends BroadcastReceiver {
+
+        @SuppressWarnings("deprecation")
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
+            traceDebug(TAG,"Reconnect since BroadcastReceiver.");
+            if (cm.getBackgroundDataSetting()) {
+                if (!backgroundDataEnabled) {
+                    backgroundDataEnabled = true;
+                    // we have the Internet connection - have another try at
+                    // connecting
+                    reconnect();
+                }
+            } else {
+                backgroundDataEnabled = false;
+                notifyClientsOffline();
+            }
+        }
+    }
+
+    /**
+     * Sets the DisconnectedBufferOptions for this client
+     * @param clientHandle identifier for the client
+     * @param bufferOpts the DisconnectedBufferOptions for this client
+     */
+    public void setBufferOpts(String clientHandle, DisconnectedBufferOptions bufferOpts) {
+        MqttConnection client = getConnection(clientHandle);
+        client.setBufferOpts(bufferOpts);
+    }
+
+    public int getBufferedMessageCount(String clientHandle){
+        MqttConnection client = getConnection(clientHandle);
+        return client.getBufferedMessageCount();
+    }
+
+    public MqttMessage getBufferedMessage(String clientHandle, int bufferIndex){
+        MqttConnection client = getConnection(clientHandle);
+        return client.getBufferedMessage(bufferIndex);
+    }
+
+    public void deleteBufferedMessage(String clientHandle, int bufferIndex){
+        MqttConnection client = getConnection(clientHandle);
+        client.deleteBufferedMessage(bufferIndex);
+    }
+
+}
+

+ 42 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/MqttServiceBinder.java

@@ -0,0 +1,42 @@
+package com.somsakelect.android.mqtt;
+
+
+import android.os.Binder;
+
+/**
+ * What the Service passes to the Activity on binding:-
+ * <ul>
+ * <li>a reference to the Service
+ * <li>the activityToken provided when the Service was started
+ * </ul>
+ *
+ */
+class MqttServiceBinder extends Binder {
+
+    private MqttService mqttService;
+    private String activityToken;
+
+    MqttServiceBinder(MqttService mqttService) {
+        this.mqttService = mqttService;
+    }
+
+    /**
+     * @return a reference to the Service
+     */
+    public MqttService getService() {
+        return mqttService;
+    }
+
+    void setActivityToken(String activityToken) {
+        this.activityToken = activityToken;
+    }
+
+    /**
+     * @return the activityToken provided when the Service was started
+     */
+    public String getActivityToken() {
+        return activityToken;
+    }
+
+}
+

+ 88 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/MqttServiceConstants.java

@@ -0,0 +1,88 @@
+package com.somsakelect.android.mqtt;
+
+
+/**
+ * Various strings used to identify operations or data in the Android MQTT
+ * service, mainly used in Intents passed between Activities and the Service.
+ */
+interface MqttServiceConstants {
+
+    /*
+     * Version information
+     */
+
+    String VERSION = "v0";
+
+    /*
+     * Attributes of messages <p> Used for the column names in the database
+     */
+    String DUPLICATE = "duplicate";
+    String RETAINED = "retained";
+    String QOS = "qos";
+    String PAYLOAD = "payload";
+    String DESTINATION_NAME = "destinationName";
+    String CLIENT_HANDLE = "clientHandle";
+    String MESSAGE_ID = "messageId";
+
+    /* Tags for actions passed between the Activity and the Service */
+    String SEND_ACTION = "send";
+    String UNSUBSCRIBE_ACTION = "unsubscribe";
+    String SUBSCRIBE_ACTION = "subscribe";
+    String DISCONNECT_ACTION = "disconnect";
+    String CONNECT_ACTION = "connect";
+    String CONNECT_EXTENDED_ACTION = "connectExtended";
+    String MESSAGE_ARRIVED_ACTION = "messageArrived";
+    String MESSAGE_DELIVERED_ACTION = "messageDelivered";
+    String ON_CONNECTION_LOST_ACTION = "onConnectionLost";
+    String TRACE_ACTION = "trace";
+
+    /* Identifies an Intent which calls back to the Activity */
+    String CALLBACK_TO_ACTIVITY = MqttService.TAG
+            + ".callbackToActivity"+"."+VERSION;
+
+    /* Identifiers for extra data on Intents broadcast to the Activity */
+    String CALLBACK_ACTION = MqttService.TAG + ".callbackAction";
+    String CALLBACK_STATUS = MqttService.TAG + ".callbackStatus";
+    String CALLBACK_CLIENT_HANDLE = MqttService.TAG + "."
+            + CLIENT_HANDLE;
+    String CALLBACK_ERROR_MESSAGE = MqttService.TAG
+            + ".errorMessage";
+    String CALLBACK_EXCEPTION_STACK = MqttService.TAG
+            + ".exceptionStack";
+    String CALLBACK_INVOCATION_CONTEXT = MqttService.TAG + "."
+            + "invocationContext";
+    String CALLBACK_ACTIVITY_TOKEN = MqttService.TAG + "."
+            + "activityToken";
+    String CALLBACK_DESTINATION_NAME = MqttService.TAG + '.'
+            + DESTINATION_NAME;
+    String CALLBACK_MESSAGE_ID = MqttService.TAG + '.'
+            + MESSAGE_ID;
+    String CALLBACK_RECONNECT = MqttService.TAG + ".reconnect";
+    String CALLBACK_SERVER_URI = MqttService.TAG + ".serverURI";
+    String CALLBACK_MESSAGE_PARCEL = MqttService.TAG + ".PARCEL";
+    String CALLBACK_TRACE_SEVERITY = MqttService.TAG
+            + ".traceSeverity";
+    String CALLBACK_TRACE_TAG = MqttService.TAG + ".traceTag";
+    String CALLBACK_TRACE_ID = MqttService.TAG + ".traceId";
+    String CALLBACK_ERROR_NUMBER = MqttService.TAG
+            + ".ERROR_NUMBER";
+
+    String CALLBACK_EXCEPTION = MqttService.TAG + ".exception";
+
+    //Intent prefix for Ping sender.
+    String PING_SENDER = MqttService.TAG + ".pingSender.";
+
+    //Constant for wakelock
+    String PING_WAKELOCK = MqttService.TAG + ".client.";
+    String WAKELOCK_NETWORK_INTENT = MqttService.TAG + "";
+
+    //Trace severity levels
+    String TRACE_ERROR = "error";
+    String TRACE_DEBUG = "debug";
+    String TRACE_EXCEPTION = "exception";
+
+
+    //exception code for non MqttExceptions
+    int NON_MQTT_EXCEPTION = -1;
+
+}

+ 242 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/MqttTokenAndroid.java

@@ -0,0 +1,242 @@
+package com.somsakelect.android.mqtt;
+
+
+import org.eclipse.paho.client.mqttv3.IMqttActionListener;
+import org.eclipse.paho.client.mqttv3.IMqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.IMqttToken;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttSecurityException;
+import org.eclipse.paho.client.mqttv3.internal.wire.MqttWireMessage;
+
+/**
+ * <p>
+ * Implementation of the IMqttToken interface for use from within the
+ * MqttAndroidClient implementation
+ */
+
+class MqttTokenAndroid implements IMqttToken {
+
+    private IMqttActionListener listener;
+
+    private volatile boolean isComplete;
+
+    private volatile MqttException lastException;
+
+    private Object waitObject = new Object();
+
+    private MqttAndroidClient client;
+
+    private Object userContext;
+
+    private String[] topics;
+
+    private IMqttToken delegate; // specifically for getMessageId
+
+    private MqttException pendingException;
+
+    /**
+     * Standard constructor
+     *
+     * @param client used to pass MqttAndroidClient object
+     * @param userContext used to pass context
+     * @param listener optional listener that will be notified when the action completes. Use null if not required.
+     */
+    MqttTokenAndroid(MqttAndroidClient client,
+                     Object userContext, IMqttActionListener listener) {
+        this(client, userContext, listener, null);
+    }
+
+    /**
+     * Constructor for use with subscribe operations
+     *
+     * @param client used to pass MqttAndroidClient object
+     * @param userContext used to pass context
+     * @param listener optional listener that will be notified when the action completes. Use null if not required.
+     * @param topics topics to subscribe to, which can include wildcards.
+     */
+    MqttTokenAndroid(MqttAndroidClient client,
+                     Object userContext, IMqttActionListener listener, String[] topics) {
+        this.client = client;
+        this.userContext = userContext;
+        this.listener = listener;
+        this.topics = topics;
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttToken#waitForCompletion()
+     */
+    @Override
+    public void waitForCompletion() throws MqttException, MqttSecurityException {
+        synchronized (waitObject) {
+            try {
+                waitObject.wait();
+            }
+            catch (InterruptedException e) {
+                // do nothing
+            }
+        }
+        if (pendingException != null) {
+            throw pendingException;
+        }
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttToken#waitForCompletion(long)
+     */
+    @Override
+    public void waitForCompletion(long timeout) throws MqttException,
+            MqttSecurityException {
+        synchronized (waitObject) {
+            try {
+                waitObject.wait(timeout);
+            }
+            catch (InterruptedException e) {
+                // do nothing
+            }
+            if (!isComplete) {
+                throw new MqttException(MqttException.REASON_CODE_CLIENT_TIMEOUT);
+            }
+            if (pendingException != null) {
+                throw pendingException;
+            }
+        }
+    }
+
+    /**
+     * notify successful completion of the operation
+     */
+    void notifyComplete() {
+        synchronized (waitObject) {
+            isComplete = true;
+            waitObject.notifyAll();
+            if (listener != null) {
+                listener.onSuccess(this);
+            }
+        }
+    }
+
+    /**
+     * notify unsuccessful completion of the operation
+     */
+    void notifyFailure(Throwable exception) {
+        synchronized (waitObject) {
+            isComplete = true;
+            if (exception instanceof MqttException) {
+                pendingException = (MqttException) exception;
+            }
+            else {
+                pendingException = new MqttException(exception);
+            }
+            waitObject.notifyAll();
+            if (exception instanceof MqttException) {
+                lastException = (MqttException) exception;
+            }
+            if (listener != null) {
+                listener.onFailure(this, exception);
+            }
+        }
+
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttToken#isComplete()
+     */
+    @Override
+    public boolean isComplete() {
+        return isComplete;
+    }
+
+    void setComplete(boolean complete) {
+        isComplete = complete;
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttToken#getException()
+     */
+    @Override
+    public MqttException getException() {
+        return lastException;
+    }
+
+    void setException(MqttException exception) {
+        lastException = exception;
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttToken#getClient()
+     */
+    @Override
+    public IMqttAsyncClient getClient() {
+        return client;
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttToken#setActionCallback(IMqttActionListener)
+     */
+    @Override
+    public void setActionCallback(IMqttActionListener listener) {
+        this.listener = listener;
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttToken#getActionCallback()
+     */
+    @Override
+    public IMqttActionListener getActionCallback() {
+        return listener;
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttToken#getTopics()
+     */
+    @Override
+    public String[] getTopics() {
+        return topics;
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttToken#setUserContext(Object)
+     */
+    @Override
+    public void setUserContext(Object userContext) {
+        this.userContext = userContext;
+
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttToken#getUserContext()
+     */
+    @Override
+    public Object getUserContext() {
+        return userContext;
+    }
+
+    void setDelegate(IMqttToken delegate) {
+        this.delegate = delegate;
+    }
+
+    /**
+     * @see org.eclipse.paho.client.mqttv3.IMqttToken#getMessageId()
+     */
+    @Override
+    public int getMessageId() {
+        return (delegate != null) ? delegate.getMessageId() : 0;
+    }
+
+    @Override
+    public MqttWireMessage getResponse() {
+        return delegate.getResponse();
+    }
+
+    @Override
+    public boolean getSessionPresent() {
+        return delegate.getSessionPresent();
+    }
+
+    @Override
+    public int[] getGrantedQos() {
+        return delegate.getGrantedQos();
+    }
+
+}
+

+ 45 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/MqttTraceHandler.java

@@ -0,0 +1,45 @@
+package com.somsakelect.android.mqtt;
+
+
+/**
+ * Interface for simple trace handling, pass the trace message to trace
+ * callback.
+ *
+ */
+
+public interface MqttTraceHandler {
+
+    /**
+     * Trace debugging information
+     *
+     * @param tag
+     *            identifier for the source of the trace
+     * @param message
+     *            the text to be traced
+     */
+    void traceDebug(String tag, String message);
+
+    /**
+     * Trace error information
+     *
+     * @param tag
+     *            identifier for the source of the trace
+     * @param message
+     *            the text to be traced
+     */
+    void traceError(String tag, String message);
+
+    /**
+     * trace exceptions
+     *
+     * @param tag
+     *            identifier for the source of the trace
+     * @param message
+     *            the text to be traced
+     * @param e
+     *            the exception
+     */
+    void traceException(String tag, String message,
+                        Exception e);
+
+}

+ 109 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/ParcelableMqttMessage.java

@@ -0,0 +1,109 @@
+package com.somsakelect.android.mqtt;
+
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+
+/**
+ * <p>
+ * A way to flow MqttMessages via Bundles/Intents
+ * </p>
+ *
+ * <p>
+ * An application will probably use this only when receiving a message from a
+ * Service in a Bundle - the necessary code will be something like this :-
+ * </p>
+ * <pre>
+ * <code>
+ * 	private void messageArrivedAction(Bundle data) {
+ * 		ParcelableMqttMessage message = (ParcelableMqttMessage) data
+ * 			.getParcelable(MqttServiceConstants.CALLBACK_MESSAGE_PARCEL);
+ *		<i>Use the normal {@link MqttMessage} methods on the the message object.</i>
+ * 	}
+ *
+ * </code>
+ * </pre>
+ *
+ * <p>
+ * It is unlikely that an application will directly use the methods which are
+ * specific to this class.
+ * </p>
+ */
+
+public class ParcelableMqttMessage extends MqttMessage implements Parcelable {
+
+    String messageId = null;
+
+    ParcelableMqttMessage(MqttMessage original) {
+        super(original.getPayload());
+        setQos(original.getQos());
+        setRetained(original.isRetained());
+        setDuplicate(original.isDuplicate());
+    }
+
+    ParcelableMqttMessage(Parcel parcel) {
+        super(parcel.createByteArray());
+        setQos(parcel.readInt());
+        boolean[] flags = parcel.createBooleanArray();
+        setRetained(flags[0]);
+        setDuplicate(flags[1]);
+        messageId = parcel.readString();
+    }
+
+    /**
+     * @return the messageId
+     */
+    public String getMessageId() {
+        return messageId;
+    }
+
+    /**
+     * Describes the contents of this object
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Writes the contents of this object to a parcel
+     *
+     * @param parcel
+     *            The parcel to write the data to.
+     * @param flags
+     *            this parameter is ignored
+     */
+    @Override
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeByteArray(getPayload());
+        parcel.writeInt(getQos());
+        parcel.writeBooleanArray(new boolean[]{isRetained(), isDuplicate()});
+        parcel.writeString(messageId);
+    }
+
+    /**
+     * A creator which creates the message object from a parcel
+     */
+    public static final Parcelable.Creator<ParcelableMqttMessage> CREATOR = new Parcelable.Creator<ParcelableMqttMessage>() {
+
+        /**
+         * Creates a message from the parcel object
+         */
+        @Override
+        public ParcelableMqttMessage createFromParcel(Parcel parcel) {
+            return new ParcelableMqttMessage(parcel);
+        }
+
+        /**
+         * creates an array of type {@link ParcelableMqttMessage}[]
+         *
+         */
+        @Override
+        public ParcelableMqttMessage[] newArray(int size) {
+            return new ParcelableMqttMessage[size];
+        }
+    };
+}
+

+ 23 - 0
mqtt/src/main/java/com/somsakelect/android/mqtt/Status.java

@@ -0,0 +1,23 @@
+package com.somsakelect.android.mqtt;
+
+
+/**
+ * Enumeration representing the success or failure of an operation
+ */
+enum Status {
+    /**
+     * Indicates that the operation succeeded
+     */
+    OK,
+
+    /**
+     * Indicates that the operation failed
+     */
+    ERROR,
+
+    /**
+     * Indicates that the operation's result may be returned asynchronously
+     */
+    NO_RESULT
+}
+

+ 17 - 0
mqtt/src/test/java/com/somsakelect/android/mqtt/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.somsakelect.android.mqtt;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 3 - 0
settings.gradle

@@ -0,0 +1,3 @@
+include ':mqtt'
+include ':app'
+rootProject.name = "My MQTT"