package im.actor.sdk.push; import android.app.Service; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.IBinder; import android.util.Log; 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.persist.MemoryPersistence; import java.util.List; import java.util.Random; import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import im.actor.runtime.util.ExponentialBackoff; /** * Actor Push service based on MQTT */ public class ActorPushService extends Service implements MqttCallback { private static final String TAG = "PushService"; private final Executor connectionExecutor = Executors.newSingleThreadExecutor(); private final MemoryPersistence persistence = new MemoryPersistence(); private final MqttConnectOptions connectOptions = new MqttConnectOptions(); private String packageName; private String receiverName; private SharedPreferences preferences; private String[] mqttUrls; private String mqttClientId; private String mqttTopic; private String mqttUsername; private String mqttPassword; private MqttClient mqttClient; private int attemptIndex = 0; private boolean isConnecting = false; private Random random = new Random(); private ExponentialBackoff exponentialBackoff = new ExponentialBackoff(1000, 5 * 60000, 15); public ActorPushService() { // Configuration on MQTT connection options connectOptions.setCleanSession(false); connectOptions.setConnectionTimeout(5 /* 5 seconds */); connectOptions.setKeepAliveInterval(15 * 60 /* 15 minutes */); } @Override public void onCreate() { Log.d(TAG, "onCreate"); super.onCreate(); // Loading Application Package packageName = getApplicationContext().getPackageName(); // // Searching for Receiver // PackageManager packageManager = getPackageManager(); Intent intent = new Intent("im.actor.push.intent.RECEIVE"); List<ResolveInfo> resolveInfoList = packageManager.queryBroadcastReceivers(intent, 0); for (ResolveInfo r : resolveInfoList) { if (packageName.equals(r.activityInfo.packageName)) { receiverName = r.activityInfo.name; break; } } // // Loading current MQTT state // preferences = getSharedPreferences("actor_push_service", MODE_PRIVATE); // // Loading unique clientId // mqttClientId = preferences.getString("mqtt_clientId", null); if (mqttClientId == null) { mqttClientId = UUID.randomUUID().toString(); preferences.edit() .putString("mqtt_clientId", mqttClientId) .commit(); } // // Loading registration info // String urls = preferences.getString("mqtt_hosts", null); if (urls != null) { mqttUrls = urls.split(","); } mqttTopic = preferences.getString("mqtt_topic", null); mqttUsername = preferences.getString("mqtt_username", null); mqttPassword = preferences.getString("mqtt_password", null); // // Starting service // if (mqttUrls == null || mqttTopic == null || mqttUsername == null || mqttPassword == null) { mqttUrls = null; mqttTopic = null; mqttUsername = null; mqttPassword = null; Log.d(TAG, "Not started"); } else { tryConnect(); } } // // Connection creation // private synchronized void connectToBroker(String[] hosts, String topic, String username, String password) { Log.d(TAG, "connectToBroker:" + hosts + ", topic:" + topic); // Cancelling old connection cancelConnection(); // Saving credentials mqttUrls = hosts; mqttTopic = topic; mqttUsername = username; mqttPassword = password; StringBuilder b = new StringBuilder(); for (int i = 0; i < hosts.length; i++) { if (i != 0) { b.append(","); } b.append(hosts[i]); } preferences.edit() .putString("mqtt_url", b.toString()) .putString("mqtt_topic", mqttTopic) .putString("mqtt_username", mqttUsername) .putString("mqtt_password", mqttPassword) .commit(); // Starting connection tryConnect(); } private synchronized void tryConnect() { Log.d(TAG, "tryConnect"); // If connected if (mqttClient != null && mqttClient.isConnected()) { Log.d(TAG, "Already connected"); return; } // Checking state if (isConnecting) { Log.d(TAG, "Already connecting"); return; } isConnecting = true; Log.d(TAG, "Starting connecting..."); connectionExecutor.execute(new Runnable() { @Override public void run() { // Clearing mqttClient if (mqttClient != null) { try { mqttClient.close(); } catch (MqttException e) { e.printStackTrace(); } } mqttClient = null; try { Thread.sleep(exponentialBackoff.exponentialWait()); } catch (InterruptedException e) { e.printStackTrace(); return; } // Setting credentials connectOptions.setUserName(mqttUsername); connectOptions.setPassword(mqttPassword.toCharArray()); // Starting mqtt connection final int attempt = ++attemptIndex; Log.d(TAG, "Connecting..."); MqttClient mqttClient; try { mqttClient = new MqttClient(mqttUrls[random.nextInt(mqttUrls.length)], mqttClientId, persistence); mqttClient.connect(connectOptions); Log.d(TAG, "Connected"); mqttClient.setCallback(ActorPushService.this); mqttClient.subscribe(mqttTopic, 1); Log.d(TAG, "Complete"); } catch (MqttException e) { Log.d(TAG, "Exception"); e.printStackTrace(); onConnectionFailure(); return; } Log.d(TAG, "Success"); onConnected(attempt, mqttClient); } }); } private synchronized void cancelConnection() { Log.d(TAG, "cancelConnection"); isConnecting = false; attemptIndex++; // Clearing mqttClient if (mqttClient != null) { try { mqttClient.close(); } catch (MqttException e) { e.printStackTrace(); } } mqttClient = null; } private synchronized void onConnected(int attempt, MqttClient mqttClient) { if (this.attemptIndex == attempt) { this.isConnecting = false; this.mqttClient = mqttClient; exponentialBackoff.onSuccess(); } else { // Incorrect attempt try { mqttClient.disconnect(); } catch (MqttException e) { e.printStackTrace(); } try { mqttClient.close(); } catch (MqttException e) { e.printStackTrace(); } } } private synchronized void onConnectionFailure() { Log.d(TAG, "Connect Failure"); // // Trying to recreate connection // isConnecting = false; exponentialBackoff.onFailure(); tryConnect(); } // // MQTT callbacks // @Override public synchronized void connectionLost(Throwable cause) { Log.d(TAG, "Connection Lost"); // Clearing connection if (mqttClient != null) { try { mqttClient.disconnect(); } catch (MqttException e) { e.printStackTrace(); } try { mqttClient.close(); } catch (MqttException e) { e.printStackTrace(); } } this.mqttClient = null; exponentialBackoff.onFailure(); // // Trying to recreate connection // tryConnect(); } @Override public synchronized void messageArrived(String topic, MqttMessage message) throws Exception { String msg = new String(message.getPayload(), "utf-8"); Log.d(TAG, "Received " + topic + " " + msg); if (packageName != null && receiverName != null) { sendBroadcast(new Intent("im.actor.push.intent.RECEIVE") .setClassName(packageName, receiverName) .putExtra("push_payload", msg)); } } @Override public synchronized void deliveryComplete(IMqttDeliveryToken token) { // Ignore } // // Internals // @Override public IBinder onBind(Intent intent) { // Do not implement binding as this will probably block from service restarting return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null && intent.hasExtra("mqtt_urls") && intent.hasExtra("mqtt_topic") && intent.hasExtra("mqtt_username") && intent.hasExtra("mqtt_password")) { String[] url = intent.getStringArrayExtra("mqtt_urls"); String topic = intent.getStringExtra("mqtt_topic"); String username = intent.getStringExtra("mqtt_username"); String password = intent.getStringExtra("mqtt_password"); connectToBroker(url, topic, username, password); } // Making Service restart after killing // TODO: May not work correctly on 4.4+ on some devices - need to implement workaround return START_STICKY; } }