package org.mercury_im.messenger.android.service; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.IBinder; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import org.jivesoftware.smackx.ping.android.ServerPingWithAlarmManager; import org.mercury_im.messenger.R; import org.mercury_im.messenger.android.MercuryImApplication; import org.mercury_im.messenger.android.Notifications; import org.mercury_im.messenger.android.ui.MainActivity; import org.mercury_im.messenger.core.connection.MercuryConnectionManager; import org.mercury_im.messenger.core.connection.state.ConnectionPoolState; import org.mercury_im.messenger.core.connection.state.ConnectionState; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import javax.inject.Inject; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; /** * Started, Bound Service, which is responsible keeping the application alive when the app is not open. */ public class MercuryForegroundService extends Service { private static final String APP = "org.mercury-im.messenger"; private static final String SERVICE = APP + ".MercuryForegroundService"; private static final String ACTION = SERVICE + ".ACTION"; // ACTIONS public static final String ACTION_START = ACTION + ".START"; public static final String ACTION_STOP = ACTION + ".STOP"; private static final Logger LOGGER = Logger.getLogger(MercuryForegroundService.class.getName()); @Inject MercuryConnectionManager connectionManager; CompositeDisposable disposable; @NonNull @Override public final IBinder onBind(Intent intent) { return new Binder(this); } @Override public void onCreate() { super.onCreate(); MercuryImApplication.getApplication().getAppComponent().inject(this); beginLifecycleOfPingManager(); disposable = new CompositeDisposable(); disposable.add(connectionManager.observeConnectionPool() .subscribe(state -> { Notification notification = buildForegroundNotification(getApplicationContext(), state); NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(Notifications.FOREGROUND_SERVICE_ID, notification); })); LOGGER.log(Level.INFO, "onCreate"); } /** * PingManager will ensure the XMPP connection is kept alive. */ private void beginLifecycleOfPingManager() { ServerPingWithAlarmManager.onCreate(this); } @Override public void onDestroy() { super.onDestroy(); disposable.dispose(); endLifecycleOfPingManager(); LOGGER.log(Level.INFO, "onDestroy"); } private void endLifecycleOfPingManager() { ServerPingWithAlarmManager.onDestroy(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent == null) { startAndDisplayForegroundNotification(); } else { String action = intent.getAction(); action = action == null ? "" : action; switch (action) { case ACTION_START: startAndDisplayForegroundNotification(); break; case ACTION_STOP: stopForeground(true); stopSelf(); break; default: break; } } return START_STICKY_COMPATIBILITY; } private void startAndDisplayForegroundNotification() { Notification notification = buildForegroundNotification(getApplicationContext(), connectionManager.observeConnectionPool().blockingFirst()); startForeground(Notifications.FOREGROUND_SERVICE_ID, notification); } private static Notification buildForegroundNotification(Context context, ConnectionPoolState state) { Intent startMainActivityIntent = new Intent(context, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, startMainActivityIntent, 0); String notificationText = notificationTextFrom(state); return new NotificationCompat.Builder(context, Notifications.NOTIFICATION_CHANNEL__FOREGROUND_SERVICE) .setSmallIcon(R.drawable.ic_mercury_black_24dp) .setContentTitle("Mercury-IM is running!") .setContentText(notificationText) .setStyle(new NotificationCompat.BigTextStyle().bigText(notificationText)) .setContentIntent(pendingIntent) .build(); } private static String notificationTextFrom(ConnectionPoolState state) { List connecting = new ArrayList<>(); List authenticated = new ArrayList<>(); List erred = new ArrayList<>(); for (UUID id : state.getConnectionStates().keySet()) { ConnectionState connectionState = state.getConnectionStates().get(id); switch (connectionState.getConnectivity()) { case disconnected: break; case connecting: case connected: connecting.add(id); break; case authenticated: authenticated.add(id); break; case disconnectedOnError: erred.add(id); break; } } StringBuilder sb = new StringBuilder(); if (!authenticated.isEmpty()) { sb.append(authenticated.size()) .append(authenticated.size() == 1 ? " account" : " accounts") .append(" connected."); } if (!connecting.isEmpty()) { if (!sb.toString().isEmpty()) { sb.append("\n"); } sb.append(connecting.size()) .append(authenticated.size() == 1 ? " account" : " accounts") .append(" connecting."); } if (!erred.isEmpty()) { if (!sb.toString().isEmpty()) { sb.append("\n"); } Iterator iterator = erred.iterator(); while (iterator.hasNext()) { UUID id = iterator.next(); sb.append(getAddress(state, id)); if (iterator.hasNext()) sb.append(", "); } sb.append(erred.size() == 1 ? " has an error." : " have errors."); } return sb.toString(); } private static String getAddress(ConnectionPoolState state, UUID uuid) { return state.getConnectionStates().get(uuid).getConnection().getAccount().getAddress(); } public class Binder extends android.os.Binder { private final MercuryForegroundService service; public Binder(MercuryForegroundService service) { this.service = service; } public MercuryForegroundService getService() { return service; } } }