add basic foreground service and event receiver

This commit is contained in:
Daniel Gultsch 2023-02-18 15:39:47 +01:00
parent d6edea8ddf
commit 2212c63810
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
10 changed files with 289 additions and 8 deletions

View file

@ -44,6 +44,7 @@
<queries> <queries>
<package android:name="org.torproject.android" /> <package android:name="org.torproject.android" />
<intent> <intent>
<action android:name="eu.siacs.conversations.location.request" /> <action android:name="eu.siacs.conversations.location.request" />
</intent> </intent>
@ -58,35 +59,49 @@
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
</intent> </intent>
<intent> <intent>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/> <action android:name="org.unifiedpush.android.connector.MESSAGE" />
</intent> </intent>
</queries> </queries>
<application <application
android:name="im.conversations.android.Conversations" android:name="im.conversations.android.Conversations"
android:allowBackup="true" android:allowBackup="true"
android:appCategory="social" android:appCategory="social"
android:icon="@mipmap/new_launcher"
android:roundIcon="@mipmap/new_launcher_round"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/new_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/new_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Conversations3" android:theme="@style/Theme.Conversations3"
tools:targetApi="31"> tools:targetApi="31">
<service
android:name=".service.ForegroundService"
android:exported="false" />
<receiver
android:name=".receiver.EventReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
<action android:name="android.media.RINGER_MODE_CHANGED" />
</intent-filter>
</receiver>
<activity <activity
android:name="im.conversations.android.ui.activity.SetupActivity" android:name="im.conversations.android.ui.activity.MainActivity"
android:exported="true" android:exported="true">
android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="im.conversations.android.ui.activity.SetupActivity"
android:windowSoftInputMode="adjustResize" />
</application> </application>
</manifest> </manifest>

View file

@ -4,6 +4,7 @@ import android.app.Application;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import com.google.android.material.color.DynamicColors; import com.google.android.material.color.DynamicColors;
import im.conversations.android.dns.Resolver; import im.conversations.android.dns.Resolver;
import im.conversations.android.notification.Channels;
import im.conversations.android.xmpp.ConnectionPool; import im.conversations.android.xmpp.ConnectionPool;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
@ -25,6 +26,8 @@ public class Conversations extends Application {
} catch (final Throwable throwable) { } catch (final Throwable throwable) {
LOGGER.warn("Could not initialize security provider", throwable); LOGGER.warn("Could not initialize security provider", throwable);
} }
final var channels = new Channels(this);
channels.initialize();
Resolver.init(this); Resolver.init(this);
ConnectionPool.getInstance(this).reconfigure(); ConnectionPool.getInstance(this).reconfigure();
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -3,6 +3,7 @@ package im.conversations.android.xmpp;
import android.content.Context; import android.content.Context;
import android.os.SystemClock; import android.os.SystemClock;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
@ -21,6 +22,7 @@ import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.jxmpp.jid.BareJid; import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -48,6 +50,8 @@ public class ConnectionPool {
private final List<XmppConnection> connections = new ArrayList<>(); private final List<XmppConnection> connections = new ArrayList<>();
private final HashSet<Jid> lowPingTimeoutMode = new HashSet<>(); private final HashSet<Jid> lowPingTimeoutMode = new HashSet<>();
private Consumer<Summary> summaryProcessor;
private ConnectionPool(final Context context) { private ConnectionPool(final Context context) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
} }
@ -207,10 +211,30 @@ public class ConnectionPool {
} }
} }
} }
this.updateSummaryProcessor();
// TODO toggle error notification // TODO toggle error notification
// getNotificationService().updateErrorNotification(); // 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<Summary> processor) {
this.summaryProcessor = processor;
}
public void scheduleWakeUpCall(final int seconds) { public void scheduleWakeUpCall(final int seconds) {
CONNECTION_SCHEDULER.schedule( CONNECTION_SCHEDULER.schedule(
() -> { () -> {
@ -372,6 +396,20 @@ public class ConnectionPool {
return ImmutableSet.copyOf(Lists.transform(this.connections, XmppConnection::getAccount)); 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) { public static ConnectionPool getInstance(final Context context) {
if (INSTANCE != null) { if (INSTANCE != null) {
return INSTANCE; return INSTANCE;

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1 0,1.43 -0.98,2.63 -2.31,2.98l1.46,1.46C20.88,15.61 22,13.95 22,12c0,-2.76 -2.24,-5 -5,-5zM16,11h-2.19l2,2L16,13zM2,4.27l3.11,3.11C3.29,8.12 2,9.91 2,12c0,2.76 2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1 0,-1.59 1.21,-2.9 2.76,-3.07L8.73,11L8,11v2h2.73L13,15.27L13,17h1.73l4.01,4L20,19.74 3.27,3 2,4.27z" />
</vector>