package st.alr.mqttitude.services;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
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.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.MqttTopic;
import org.json.JSONException;
import org.json.JSONObject;
import st.alr.mqttitude.App;
import st.alr.mqttitude.R;
import st.alr.mqttitude.support.Defaults;
import st.alr.mqttitude.support.Events;
import st.alr.mqttitude.support.MqttPublish;
import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.provider.Settings.Secure;
import android.util.Log;
import de.greenrobot.event.EventBus;
public class ServiceMqtt extends ServiceBindable implements MqttCallback
{
public static enum MQTT_CONNECTIVITY {
INITIAL, CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED_WAITINGFORINTERNET, DISCONNECTED_USERDISCONNECT, DISCONNECTED_DATADISABLED, DISCONNECTED, DISCONNECTED_ERROR
}
private static MQTT_CONNECTIVITY mqttConnectivity = MQTT_CONNECTIVITY.DISCONNECTED;
private short keepAliveSeconds;
private String mqttClientId;
private MqttClient mqttClient;
private static SharedPreferences sharedPreferences;
private static ServiceMqtt instance;
private SharedPreferences.OnSharedPreferenceChangeListener preferencesChangedListener;
private Thread workerThread;
private LinkedList<DeferredPublishable> deferredPublishables;
private static MqttException error;
private HandlerThread pubThread;
private Handler pubHandler;
// An alarm for rising in special times to fire the
// pendingIntentPositioning
private AlarmManager alarmManagerPositioning;
// A PendingIntent for calling a receiver in special times
public PendingIntent pendingIntentPositioning;
//handle any deferred subscriptions because of lack of connectivity
private ArrayList<String> deferredSubscriptions = new ArrayList<String>();
//contacts vars
private Uri CONTENT_URI = ContactsContract.Contacts.CONTENT_URI;
/**
* @category SERVICE HANDLING
*/
@Override
public void onCreate()
{
super.onCreate();
instance = this;
workerThread = null;
error = null;
changeMqttConnectivity(MQTT_CONNECTIVITY.INITIAL);
keepAliveSeconds = 15 * 60;
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
deferredPublishables = new LinkedList<DeferredPublishable>();
EventBus.getDefault().register(this);
pubThread = new HandlerThread("MQTTPUBTHREAD");
pubThread.start();
pubHandler = new Handler(pubThread.getLooper());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
doStart(intent, startId);
return super.onStartCommand(intent, flags, startId);
}
private void doStart(final Intent intent, final int startId) {
// init();
Thread thread1 = new Thread() {
@Override
public void run() {
handleStart(intent, startId);
if (this == workerThread) // Clean up worker thread
workerThread = null;
}
@Override
public void interrupt() {
if (this == workerThread) // Clean up worker thread
workerThread = null;
super.interrupt();
}
};
thread1.start();
}
void handleStart(Intent intent, int startId) {
Log.v(this.toString(), "handleStart");
// Respect user's wish to stay disconnected. Overwrite with startId == -1 to reconnect manually afterwards
if ((mqttConnectivity == MQTT_CONNECTIVITY.DISCONNECTED_USERDISCONNECT) && startId != -1) {
Log.d(this.toString(), "handleStart: respecting user disconnect ");
return;
}
// No need to connect if we're already connecting
if (isConnecting()) {
Log.d(this.toString(), "handleStart: already connecting");
return;
}
// Respect user's wish to not use data
if (!isBackgroundDataEnabled()) {
Log.e(this.toString(), "handleStart: !isBackgroundDataEnabled");
changeMqttConnectivity(MQTT_CONNECTIVITY.DISCONNECTED_DATADISABLED);
return;
}
// Don't do anything unless we're disconnected
if (isDisconnected())
{
Log.v(this.toString(), "handleStart: !isConnected");
// Check if there is a data connection
if (isOnline(true))
{
if (connect())
{
Log.v(this.toString(), "handleStart: connec sucessfull");
onConnect();
}
}
else
{
Log.e(this.toString(), "handleStart: !isOnline");
changeMqttConnectivity(MQTT_CONNECTIVITY.DISCONNECTED_WAITINGFORINTERNET);
}
} else {
Log.d(this.toString(), "handleStart: already connected");
}
}
private boolean isDisconnected(){
return mqttConnectivity == MQTT_CONNECTIVITY.INITIAL || mqttConnectivity == MQTT_CONNECTIVITY.DISCONNECTED || mqttConnectivity == MQTT_CONNECTIVITY.DISCONNECTED_USERDISCONNECT || mqttConnectivity == MQTT_CONNECTIVITY.DISCONNECTED_WAITINGFORINTERNET || mqttConnectivity == MQTT_CONNECTIVITY.DISCONNECTED_ERROR;
}
/**
* @category CONNECTION HANDLING
*/
private void init()
{
Log.v(this.toString(), "initMqttClient");
if (mqttClient != null) {
return;
}
try
{
String brokerAddress = sharedPreferences.getString(Defaults.SETTINGS_KEY_BROKER_HOST,
Defaults.VALUE_BROKER_HOST);
String brokerPort = sharedPreferences.getString(Defaults.SETTINGS_KEY_BROKER_PORT,
Defaults.VALUE_BROKER_PORT);
String prefix = getBrokerSecurityMode() == Defaults.VALUE_BROKER_SECURITY_NONE ? "tcp"
: "ssl";
mqttClient = new MqttClient(prefix + "://" + brokerAddress + ":" + brokerPort,
getClientId(), null);
mqttClient.setCallback(this);
} catch (MqttException e)
{
// something went wrong!
mqttClient = null;
changeMqttConnectivity(MQTT_CONNECTIVITY.DISCONNECTED);
}
}
private int getBrokerSecurityMode() {
return sharedPreferences.getInt(Defaults.SETTINGS_KEY_BROKER_SECURITY,
Defaults.VALUE_BROKER_SECURITY_NONE);
}
//
private javax.net.ssl.SSLSocketFactory getSSLSocketFactory() throws CertificateException,
KeyStoreException, NoSuchAlgorithmException, IOException, KeyManagementException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// From https://www.washington.edu/itconnect/security/ca/load-der.crt
InputStream caInput = new BufferedInputStream(new FileInputStream(
sharedPreferences.getString(Defaults.SETTINGS_KEY_BROKER_SECURITY_SSL_CA_PATH, "")));
java.security.cert.Certificate ca;
try {
ca = cf.generateCertificate(caInput);
} finally {
caInput.close();
}
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
return context.getSocketFactory();
}
private boolean connect()
{
workerThread = Thread.currentThread(); // We connect, so we're the
// worker thread
Log.v(this.toString(), "connect");
init();
try
{
changeMqttConnectivity(MQTT_CONNECTIVITY.CONNECTING);
MqttConnectOptions options = new MqttConnectOptions();
if (getBrokerSecurityMode() == Defaults.VALUE_BROKER_SECURITY_SSL_CUSTOMCACRT)
options.setSocketFactory(this.getSSLSocketFactory());
if (!sharedPreferences.getString(Defaults.SETTINGS_KEY_BROKER_PASSWORD, "").equals(""))
options.setPassword(sharedPreferences.getString(
Defaults.SETTINGS_KEY_BROKER_PASSWORD, "").toCharArray());
if (!sharedPreferences.getString(Defaults.SETTINGS_KEY_BROKER_USERNAME, "").equals(""))
options.setUserName(sharedPreferences.getString(
Defaults.SETTINGS_KEY_BROKER_USERNAME, ""));
//setWill(options);
options.setKeepAliveInterval(keepAliveSeconds);
options.setConnectionTimeout(30);
mqttClient.connect(options);
Log.d(this.toString(), "No error during connect");
changeMqttConnectivity(MQTT_CONNECTIVITY.CONNECTED);
//TODO: Set subscribe topic properly from UI shared prefs
//sharedPreferences.getString(Defaults.SETTINGS_KEY_TOPIC, Defaults.VALUE_TOPIC)
//we subscribe to our own channel based on username.. it will be unqiue to that user
String topic = sharedPreferences.getString(Defaults.SETTINGS_KEY_BROKER_USERNAME, "anonymous");
mqttClient.subscribe("/mqttitude/" + topic, 1);
//Add the stored subscriptions from the user preferences, we currently also parse this strng in the UI
//and I need to change it so we have an event that the UI parses to say we have a new subscription
/*String tmp = sharedPreferences.getString(Defaults.SETTING_PEER_USERNAMES, "");
if(tmp.contains(",")){
String[] usernames = tmp.split(",");
for(String u: usernames){
String peername = u.substring(u.lastIndexOf("/")+1);
//this is hacky and just a postback to self via the event bus
//Events.NewPeerAdded msg = new Events.NewPeerAdded(peername);
//EventBus.getDefault().post(msg);
}
} */
//new friends user ids by using android contacts,
//TODO: put in fn
ContentResolver cr = getContentResolver();
Cursor cur = cr.query(ContactsContract.Contacts.CONTENT_URI,
null, null, null, null);
if (cur.getCount() > 0) {
while (cur.moveToNext()) {
String id = cur.getString(cur.getColumnIndex(ContactsContract.Contacts._ID));
String name = cur.getString(cur.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
if (Integer.parseInt(cur.getString(cur.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER))) > 0) {
//Query IM details
String imWhere = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
String[] imWhereParams = new String[]{id,
ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE};
Cursor imCur = cr.query(ContactsContract.Data.CONTENT_URI,
null, imWhere, imWhereParams, null);
if (imCur.moveToFirst()) {
String imName = imCur.getString(imCur.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA));
String imType;
imType = imCur.getString(imCur.getColumnIndex(ContactsContract.CommonDataKinds.Im.TYPE));
String label = imCur.getString(imCur.getColumnIndex(ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL));
if(imType.equalsIgnoreCase("3")){
//TODO: CHange hard coded string
if(label.equalsIgnoreCase("MQTTITUDE")){
Events.NewPeerAdded msg = new Events.NewPeerAdded(imName);
EventBus.getDefault().post(msg);
}
}
}
imCur.close();
}
}
}
return true;
} catch (MqttException e) { // Catch paho and socket factory exceptions
Log.e(this.toString(), e.toString());
changeMqttConnectivity(MQTT_CONNECTIVITY.DISCONNECTED_ERROR, e);
return false;
} catch (Exception e) {
e.printStackTrace();
changeMqttConnectivity(MQTT_CONNECTIVITY.DISCONNECTED);
return false;
}
}
private void setWill(MqttConnectOptions m) {
StringBuffer payload = new StringBuffer();
//added unique ID for this app
String uid = sharedPreferences.getString(Defaults.SETTINGS_KEY_BROKER_USERNAME, "unknown");
payload.append("{");
payload.append("\"type\": ").append("\"").append("_lwt").append("\"");
payload.append(", \"tst\": ").append("\"").append((int) (new Date().getTime() / 1000))
.append("\"");
payload.append(", \"usr\": ").append("\"").append(uid).append("\"");
payload.append("}");
m.setWill(mqttClient.getTopic(sharedPreferences.getString(Defaults.SETTINGS_KEY_TOPIC,
Defaults.VALUE_TOPIC)), payload.toString().getBytes(), 0, false);
}
private void onConnect() {
if (!isConnected())
Log.e(this.toString(), "onConnect: !isConnected");
}
public void disconnect(boolean fromUser)
{
Log.v(this.toString(), "disconnect");
if (fromUser)
changeMqttConnectivity(MQTT_CONNECTIVITY.DISCONNECTED_USERDISCONNECT);
try
{
if (isConnected())
mqttClient.disconnect();
} catch (Exception e) {
e.printStackTrace();
} finally {
mqttClient = null;
if (workerThread != null) {
workerThread.interrupt();
}
}
}
@SuppressLint("Wakelock")
// Lint check derps with the wl.release() call.
@Override
public void connectionLost(Throwable t)
{
Log.e(this.toString(), "error: " + t.toString());
// we protect against the phone switching off while we're doing this
// 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);
WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MQTT");
wl.acquire();
if (!isOnline(true))
{
changeMqttConnectivity(MQTT_CONNECTIVITY.DISCONNECTED_WAITINGFORINTERNET);
}
else
{
changeMqttConnectivity(MQTT_CONNECTIVITY.DISCONNECTED);
}
wl.release();
}
public void reconnect() {
disconnect(true);
doStart(null, -1);
}
public void messageArrived(MqttTopic topic, MqttMessage message) throws MqttException {
String msg = new String(message.getPayload());
try {
JSONObject jObject = new JSONObject(msg);
String lat = jObject.getString("lat");
String lng = jObject.getString("lon");
String acc = jObject.getString("acc");
String tst = jObject.getString("tst");
String alt = jObject.getString("alt");
String gca = "Not sent";
try{
gca = jObject.getString("gca");
} catch (JSONException e){
//catch other clients who don't publish Geocoded Addresses
}
//strip the /mqqtitude/ from the topic to get the username
Events.UpdatedPeerLocation updatedPeerLoc = new Events.UpdatedPeerLocation(lng,lat,tst,alt,topic.getName().substring(topic.getName().lastIndexOf("/")+1),acc,gca);
EventBus.getDefault().post(updatedPeerLoc);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void onEvent(Events.MqttConnectivityChanged event) {
mqttConnectivity = event.getConnectivity();
if (event.getConnectivity() == MQTT_CONNECTIVITY.CONNECTED){
publishDeferrables();
subscribeDeferrables();
}
}
/**
* Incoming event when a new peer/friend is added
* here we subsribe to their topic *
*/
public void onEvent(Events.NewPeerAdded newPeer){
if(mqttConnectivity == MQTT_CONNECTIVITY.CONNECTED ){
try {
mqttClient.subscribe("/mqttitude/" + newPeer.getPeerUserName(), 1);
} catch (MqttException e) {
// TODO Auto-generated catch block
// TODO: Throw some UI if we fail to subscribe... could be ACL issues here?
// TODO: could also be a disconnected broker
e.printStackTrace();
//defer the subscription
deferredSubscriptions.add("/mqttitude/" + newPeer.getPeerUserName());
}
} else{
//defer the subscription
deferredSubscriptions.add("/mqttitude/" + newPeer.getPeerUserName());
}
}
/**
* @category CONNECTIVITY STATUS
*/
private void changeMqttConnectivity(MQTT_CONNECTIVITY newConnectivity, MqttException e) {
error = e;
changeMqttConnectivity(newConnectivity);
}
private void changeMqttConnectivity(MQTT_CONNECTIVITY newConnectivity) {
Log.d(this.toString(), "Connectivity changed to: " + newConnectivity);
EventBus.getDefault().post(new Events.MqttConnectivityChanged(newConnectivity));
mqttConnectivity = newConnectivity;
if(newConnectivity == MQTT_CONNECTIVITY.DISCONNECTED) {
Log.e(this.toString(), " disconnect");
}
}
private boolean isOnline(boolean shouldCheckIfOnWifi)
{
ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getActiveNetworkInfo();
return netInfo != null
// && (!shouldCheckIfOnWifi || (netInfo.getType() ==
// ConnectivityManager.TYPE_WIFI))
&& netInfo.isAvailable()
&& netInfo.isConnected();
}
public boolean isConnected()
{
return ((mqttClient != null) && (mqttClient.isConnected() == true));
}
public static boolean isErrorState(MQTT_CONNECTIVITY c) {
return c == MQTT_CONNECTIVITY.DISCONNECTED_ERROR;
}
public static boolean hasError(){
return error != null;
}
public boolean isConnecting() {
return (mqttClient != null) && mqttConnectivity == MQTT_CONNECTIVITY.CONNECTING;
}
private boolean isBackgroundDataEnabled() {
return isOnline(false);
}
public static MQTT_CONNECTIVITY getConnectivity() {
return mqttConnectivity;
}
/**
* @category MISC
*/
public static ServiceMqtt getInstance() {
return instance;
}
private String getClientId()
{
if (mqttClientId == null)
{
mqttClientId = Secure.getString(getContentResolver(), Secure.ANDROID_ID);
// MQTT specification doesn't allow client IDs longer than 23 chars
if (mqttClientId.length() > 22)
mqttClientId = mqttClientId.substring(0, 22);
}
return mqttClientId;
}
@Override
public void onDestroy()
{
// disconnect immediately
disconnect(false);
changeMqttConnectivity(MQTT_CONNECTIVITY.DISCONNECTED);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferencesChangedListener);
if (this.alarmManagerPositioning != null)
this.alarmManagerPositioning.cancel(pendingIntentPositioning);
super.onDestroy();
}
public static String getConnectivityText() {
MQTT_CONNECTIVITY c = getConnectivity();
if(isErrorState(c) && hasError())
return error.toString();
switch (c) {
case CONNECTED:
return App.getInstance().getString(R.string.connectivityConnected);
case CONNECTING:
return App.getInstance().getString(R.string.connectivityConnecting);
case DISCONNECTING:
return App.getInstance().getString(R.string.connectivityDisconnecting);
default:
return App.getInstance().getString(R.string.connectivityDisconnected);
}
}
private void deferPublish(final DeferredPublishable p) {
p.wait(deferredPublishables, new Runnable() {
@Override
public void run() {
deferredPublishables.remove(p);
if(!p.isPublishing())//might happen that the publish is in progress while the timeout occurs.
p.publishFailed();
}
});
}
public void publish(String topic, String payload) {
publish(topic, payload, false, 0, 0, null, null);
}
public void publish(String topic, String payload, boolean retained) {
publish(topic, payload, retained, 0, 0, null, null);
}
public void publish(final String topic, final String payload, final boolean retained, final int qos, final int timeout,
final MqttPublish callback, final Object extra) {
publish(new DeferredPublishable(topic, payload, retained, qos, timeout, callback, extra));
}
private void publish(final DeferredPublishable p) {
pubHandler.post(new Runnable() {
@Override
public void run() {
if(Looper.getMainLooper().getThread() == Thread.currentThread()){
Log.e(this.toString(), "PUB ON MAIN THREAD");
} else {
Log.d(this.toString(), "pub on background thread");
}
if (!isOnline(false) || !isConnected()) {
Log.d(this.toString(), "pub deferred");
deferPublish(p);
return;
}
try
{
p.publishing();
mqttClient.getTopic(p.getTopic()).publish(p);
p.publishSuccessfull();
} catch (MqttException e)
{
Log.e(this.toString(), e.getMessage());
e.printStackTrace();
p.cancelWait();
p.publishFailed();
}
}
});
}
private void publishDeferrables() {
for (Iterator<DeferredPublishable> iter = deferredPublishables.iterator(); iter.hasNext(); ) {
DeferredPublishable p = iter.next();
iter.remove();
publish(p);
}
}
private void subscribeDeferrables(){
for (Iterator<String> iter = deferredSubscriptions.iterator(); iter.hasNext(); ) {
String s = iter.next();
iter.remove();
try {
mqttClient.subscribe(s,1);
} catch (MqttException e) {
// TODO Auto-generated catch block
e.printStackTrace();
deferredSubscriptions.add(s);
}
}
//deferredSubscriptions.add(s);
}
private class DeferredPublishable extends MqttMessage {
private Handler timeoutHandler;
private MqttPublish callback;
private String topic;
private int timeout = 0;
private boolean isPublishing;
private Object extra;
public DeferredPublishable(String topic, String payload, boolean retained, int qos,
int timeout, MqttPublish callback, Object extra) {
super(payload.getBytes());
this.setQos(qos);
this.setRetained(retained);
this.extra = extra;
this.callback = callback;
this.topic = topic;
this.timeout = timeout;
}
public void publishFailed() {
if (callback != null)
callback.publishFailed(extra);
}
public void publishSuccessfull() {
if (callback != null)
callback.publishSuccessfull(extra);
cancelWait();
}
public void publishing() {
isPublishing = true;
if (callback != null)
callback.publishing(extra);
}
public boolean isPublishing(){
return isPublishing;
}
public String getTopic() {
return topic;
}
public void cancelWait(){
if(timeoutHandler != null)
this.timeoutHandler.removeCallbacksAndMessages(this);
}
public void wait(LinkedList<DeferredPublishable> queue, Runnable onRemove) {
if (timeoutHandler != null) {
Log.d(this.toString(), "This DeferredPublishable already has a timeout set");
return;
}
// No need signal waiting for timeouts of 0. The command will be
// failed right away
if (callback != null && timeout > 0)
callback.publishWaiting(extra);
queue.addLast(this);
this.timeoutHandler = new Handler();
this.timeoutHandler.postDelayed(onRemove, timeout * 1000);
}
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
String msg = new String(message.getPayload());
try {
JSONObject jObject = new JSONObject(msg);
String lat = jObject.getString("lat");
String lng = jObject.getString("lon");
String acc = jObject.getString("acc");
String tst = jObject.getString("tst");
String alt = jObject.getString("alt");
String gca = jObject.getString("gca");
//strip the /mqqtitude/ from the topic to get the username
Events.UpdatedPeerLocation updatedPeerLoc = new Events.UpdatedPeerLocation(lng,lat,tst,alt,topic.substring(topic.lastIndexOf("/")+1),acc,gca);
EventBus.getDefault().post(updatedPeerLoc);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//post event
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {}
@Override
protected void onStartOnce() {}
}