diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 808baf278..37564f17e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -44,6 +44,7 @@
+
@@ -58,35 +59,49 @@
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+ android:name="im.conversations.android.ui.activity.MainActivity"
+ android:exported="true">
+
\ No newline at end of file
diff --git a/app/src/main/java/im/conversations/android/Conversations.java b/app/src/main/java/im/conversations/android/Conversations.java
index d11f705b8..c7982790b 100644
--- a/app/src/main/java/im/conversations/android/Conversations.java
+++ b/app/src/main/java/im/conversations/android/Conversations.java
@@ -4,6 +4,7 @@ import android.app.Application;
import androidx.appcompat.app.AppCompatDelegate;
import com.google.android.material.color.DynamicColors;
import im.conversations.android.dns.Resolver;
+import im.conversations.android.notification.Channels;
import im.conversations.android.xmpp.ConnectionPool;
import java.security.SecureRandom;
import java.security.Security;
@@ -25,6 +26,8 @@ public class Conversations extends Application {
} catch (final Throwable throwable) {
LOGGER.warn("Could not initialize security provider", throwable);
}
+ final var channels = new Channels(this);
+ channels.initialize();
Resolver.init(this);
ConnectionPool.getInstance(this).reconfigure();
AppCompatDelegate.setDefaultNightMode(
diff --git a/app/src/main/java/im/conversations/android/notification/Channels.java b/app/src/main/java/im/conversations/android/notification/Channels.java
new file mode 100644
index 000000000..af671a94b
--- /dev/null
+++ b/app/src/main/java/im/conversations/android/notification/Channels.java
@@ -0,0 +1,58 @@
+package im.conversations.android.notification;
+
+import android.app.Application;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.app.NotificationManager;
+import android.os.Build;
+import androidx.annotation.RequiresApi;
+import androidx.core.content.ContextCompat;
+import im.conversations.android.R;
+
+public final class Channels {
+
+ private final Application application;
+
+ private static final String CHANNEL_GROUP_STATUS = "status";
+ static final String CHANNEL_FOREGROUND = "foreground";
+
+ public Channels(final Application application) {
+ this.application = application;
+ }
+
+ public void initialize() {
+ final var notificationManager =
+ ContextCompat.getSystemService(application, NotificationManager.class);
+ if (notificationManager == null) {
+ return;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ this.initializeGroups(notificationManager);
+ this.initializeForegroundChannel(notificationManager);
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private void initializeGroups(NotificationManager notificationManager) {
+ notificationManager.createNotificationChannelGroup(
+ new NotificationChannelGroup(
+ CHANNEL_GROUP_STATUS,
+ application.getString(R.string.notification_group_status_information)));
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private void initializeForegroundChannel(final NotificationManager notificationManager) {
+ final NotificationChannel foregroundServiceChannel =
+ new NotificationChannel(
+ CHANNEL_FOREGROUND,
+ application.getString(R.string.foreground_service_channel_name),
+ NotificationManager.IMPORTANCE_MIN);
+ foregroundServiceChannel.setDescription(
+ application.getString(
+ R.string.foreground_service_channel_description,
+ application.getString(R.string.app_name)));
+ foregroundServiceChannel.setShowBadge(false);
+ foregroundServiceChannel.setGroup(CHANNEL_GROUP_STATUS);
+ notificationManager.createNotificationChannel(foregroundServiceChannel);
+ }
+}
diff --git a/app/src/main/java/im/conversations/android/notification/ForegroundServiceNotification.java b/app/src/main/java/im/conversations/android/notification/ForegroundServiceNotification.java
new file mode 100644
index 000000000..ded7b2f90
--- /dev/null
+++ b/app/src/main/java/im/conversations/android/notification/ForegroundServiceNotification.java
@@ -0,0 +1,63 @@
+package im.conversations.android.notification;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Build;
+
+import androidx.core.content.ContextCompat;
+
+import im.conversations.android.R;
+import im.conversations.android.ui.activity.MainActivity;
+import im.conversations.android.xmpp.ConnectionPool;
+
+public class ForegroundServiceNotification {
+
+ public static final int ID = 1;
+
+ private final Service service;
+
+ public ForegroundServiceNotification(final Service service) {
+ this.service = service;
+ }
+
+ public Notification build(final ConnectionPool.Summary summary) {
+ final Notification.Builder builder = new Notification.Builder(service);
+ builder.setContentTitle(service.getString(R.string.app_name));
+ builder.setContentText(
+ service.getString(R.string.connected_accounts, summary.connected, summary.total));
+ builder.setContentIntent(buildPendingIntent());
+ builder.setWhen(0)
+ .setPriority(Notification.PRIORITY_MIN)
+ .setSmallIcon(
+ summary.isConnected()
+ ? R.drawable.ic_link_24dp
+ : R.drawable.ic_link_off_24dp)
+ .setLocalOnly(true);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ builder.setChannelId(Channels.CHANNEL_FOREGROUND);
+ }
+
+ return builder.build();
+ }
+
+ private PendingIntent buildPendingIntent() {
+ return PendingIntent.getActivity(
+ service,
+ 0,
+ new Intent(service, MainActivity.class),
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ public void update(final ConnectionPool.Summary summary) {
+ final var notificationManager = ContextCompat.getSystemService(service, NotificationManager.class);
+ if (notificationManager == null) {
+ return;
+ }
+ final var notification = build(summary);
+ notificationManager.notify(ID, notification);
+ }
+}
diff --git a/app/src/main/java/im/conversations/android/receiver/EventReceiver.java b/app/src/main/java/im/conversations/android/receiver/EventReceiver.java
new file mode 100644
index 000000000..c7b01b9a3
--- /dev/null
+++ b/app/src/main/java/im/conversations/android/receiver/EventReceiver.java
@@ -0,0 +1,20 @@
+package im.conversations.android.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import androidx.core.content.ContextCompat;
+import im.conversations.android.service.ForegroundService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class EventReceiver extends BroadcastReceiver {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(EventReceiver.class);
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ LOGGER.info("Received event {}", intent.getAction());
+ ForegroundService.start(context);
+ }
+}
diff --git a/app/src/main/java/im/conversations/android/service/ForegroundService.java b/app/src/main/java/im/conversations/android/service/ForegroundService.java
new file mode 100644
index 000000000..b0eb8dcfa
--- /dev/null
+++ b/app/src/main/java/im/conversations/android/service/ForegroundService.java
@@ -0,0 +1,49 @@
+package im.conversations.android.service;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.LifecycleService;
+import im.conversations.android.notification.ForegroundServiceNotification;
+import im.conversations.android.xmpp.ConnectionPool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ForegroundService extends LifecycleService {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ForegroundService.class);
+
+ private final ForegroundServiceNotification foregroundServiceNotification =
+ new ForegroundServiceNotification(this);
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ LOGGER.info("Creating service");
+ final var pool = ConnectionPool.getInstance(this);
+ startForeground(
+ ForegroundServiceNotification.ID,
+ foregroundServiceNotification.build(pool.buildSummary()));
+ pool.setSummaryProcessor(this::onSummaryUpdated);
+ }
+
+ private void onSummaryUpdated(final ConnectionPool.Summary summary) {
+ foregroundServiceNotification.update(summary);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ LOGGER.debug("Destroying service. Removing listeners");
+ ConnectionPool.getInstance(this).setSummaryProcessor(null);
+ }
+
+ public static void start(final Context context) {
+ try {
+ ContextCompat.startForegroundService(context, new Intent(context, ForegroundService.class));
+ } catch (final RuntimeException e) {
+ LOGGER.error("Could not start foreground service", e);
+ }
+ }
+}
diff --git a/app/src/main/java/im/conversations/android/ui/activity/MainActivity.java b/app/src/main/java/im/conversations/android/ui/activity/MainActivity.java
new file mode 100644
index 000000000..ab1cd4df6
--- /dev/null
+++ b/app/src/main/java/im/conversations/android/ui/activity/MainActivity.java
@@ -0,0 +1,15 @@
+package im.conversations.android.ui.activity;
+
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+
+import im.conversations.android.service.ForegroundService;
+
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ForegroundService.start(this);
+ }
+}
diff --git a/app/src/main/java/im/conversations/android/xmpp/ConnectionPool.java b/app/src/main/java/im/conversations/android/xmpp/ConnectionPool.java
index 6409aca8d..f6d426d1a 100644
--- a/app/src/main/java/im/conversations/android/xmpp/ConnectionPool.java
+++ b/app/src/main/java/im/conversations/android/xmpp/ConnectionPool.java
@@ -3,6 +3,7 @@ package im.conversations.android.xmpp;
import android.content.Context;
import android.os.SystemClock;
import com.google.common.base.Optional;
+import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -21,6 +22,7 @@ import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid;
import org.slf4j.Logger;
@@ -48,6 +50,8 @@ public class ConnectionPool {
private final List connections = new ArrayList<>();
private final HashSet lowPingTimeoutMode = new HashSet<>();
+ private Consumer summaryProcessor;
+
private ConnectionPool(final Context context) {
this.context = context.getApplicationContext();
}
@@ -207,10 +211,30 @@ public class ConnectionPool {
}
}
}
+ this.updateSummaryProcessor();
// TODO toggle error notification
// getNotificationService().updateErrorNotification();
}
+ private void updateSummaryProcessor() {
+ final var processor = this.summaryProcessor;
+ if (processor == null) {
+ return;
+ }
+ processor.accept(buildSummary());
+ }
+
+ public synchronized Summary buildSummary() {
+ final int connected =
+ Collections2.filter(this.connections, c -> c.getStatus() == ConnectionState.ONLINE)
+ .size();
+ return new Summary(this.connections.size(), connected);
+ }
+
+ public void setSummaryProcessor(final Consumer processor) {
+ this.summaryProcessor = processor;
+ }
+
public void scheduleWakeUpCall(final int seconds) {
CONNECTION_SCHEDULER.schedule(
() -> {
@@ -372,6 +396,20 @@ public class ConnectionPool {
return ImmutableSet.copyOf(Lists.transform(this.connections, XmppConnection::getAccount));
}
+ public static final class Summary {
+ public final int total;
+ public final int connected;
+
+ public Summary(int total, int connected) {
+ this.total = total;
+ this.connected = connected;
+ }
+
+ public boolean isConnected() {
+ return total > 0 && total == connected;
+ }
+ }
+
public static ConnectionPool getInstance(final Context context) {
if (INSTANCE != null) {
return INSTANCE;
diff --git a/app/src/main/res/drawable/ic_link_24dp.xml b/app/src/main/res/drawable/ic_link_24dp.xml
new file mode 100644
index 000000000..ccb142a61
--- /dev/null
+++ b/app/src/main/res/drawable/ic_link_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_link_off_24dp.xml b/app/src/main/res/drawable/ic_link_off_24dp.xml
new file mode 100644
index 000000000..a6bf67c09
--- /dev/null
+++ b/app/src/main/res/drawable/ic_link_off_24dp.xml
@@ -0,0 +1,10 @@
+
+
+