add basic foreground service and event receiver
This commit is contained in:
parent
d6edea8ddf
commit
2212c63810
|
@ -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>
|
||||||
|
@ -63,30 +64,44 @@
|
||||||
</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>
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
10
app/src/main/res/drawable/ic_link_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_link_24dp.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_link_off_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_link_off_24dp.xml
Normal 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>
|
Loading…
Reference in a new issue