content: 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;
      }
  }