Signal-Android/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java

317 wiersze
11 KiB
Java

package org.thoughtcrime.securesms.service;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import org.signal.core.util.PendingIntentFlags;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil;
import org.thoughtcrime.securesms.jobs.UnableToStartException;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.whispersystems.signalservice.api.util.Preconditions;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
public final class GenericForegroundService extends Service {
private static final String TAG = Log.tag(GenericForegroundService.class);
private final IBinder binder = new LocalBinder();
private static final int NOTIFICATION_ID = 827353982;
private static final String EXTRA_TITLE = "extra_title";
private static final String EXTRA_CHANNEL_ID = "extra_channel_id";
private static final String EXTRA_ICON_RES = "extra_icon_res";
private static final String EXTRA_ID = "extra_id";
private static final String EXTRA_PROGRESS = "extra_progress";
private static final String EXTRA_PROGRESS_MAX = "extra_progress_max";
private static final String EXTRA_PROGRESS_INDETERMINATE = "extra_progress_indeterminate";
private static final String ACTION_START = "start";
private static final String ACTION_STOP = "stop";
private static final AtomicInteger NEXT_ID = new AtomicInteger();
private final LinkedHashMap<Integer, Entry> allActiveMessages = new LinkedHashMap<>();
private static final Entry DEFAULTS = new Entry("", NotificationChannels.OTHER, R.drawable.ic_notification, -1, 0, 0, false);
private @Nullable Entry lastPosted;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
throw new IllegalStateException("Intent needs to be non-null.");
}
synchronized (GenericForegroundService.class) {
String action = intent.getAction();
if (action != null) {
if (ACTION_START.equals(action)) handleStart(intent);
else if (ACTION_STOP .equals(action)) handleStop(intent);
else throw new IllegalStateException(String.format("Action needs to be %s or %s.", ACTION_START, ACTION_STOP));
updateNotification();
}
return START_NOT_STICKY;
}
}
private synchronized void updateNotification() {
Iterator<Entry> iterator = allActiveMessages.values().iterator();
if (iterator.hasNext()) {
postObligatoryForegroundNotification(iterator.next());
} else {
Log.i(TAG, "Last request. Ending foreground service.");
postObligatoryForegroundNotification(lastPosted != null ? lastPosted : DEFAULTS);
stopForeground(true);
stopSelf();
}
}
private synchronized void handleStart(@NonNull Intent intent) {
Entry entry = Entry.fromIntent(intent);
Log.i(TAG, String.format(Locale.US, "handleStart() %s", entry));
allActiveMessages.put(entry.id, entry);
}
private synchronized void handleStop(@NonNull Intent intent) {
Log.i(TAG, "handleStop()");
int id = intent.getIntExtra(EXTRA_ID, -1);
Entry removed = allActiveMessages.remove(id);
if (removed == null) {
Log.w(TAG, "Could not find entry to remove");
}
}
private void postObligatoryForegroundNotification(@NonNull Entry active) {
lastPosted = active;
// TODO [greyson] Navigation
startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this, active.channelId)
.setSmallIcon(active.iconRes)
.setContentTitle(active.title)
.setProgress(active.progressMax, active.progress, active.indeterminate)
.setContentIntent(PendingIntent.getActivity(this, 0, MainActivity.clearTop(this), PendingIntentFlags.mutable()))
.setVibrate(new long[]{0})
.build());
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
/**
* Waits for {@param delayMillis} ms before starting the foreground task.
* <p>
* The delayed notification controller can also shown on demand and promoted to a regular notification controller to update the message etc.
*
* Do not call this method on API > 31
*/
public static DelayedNotificationController startForegroundTaskDelayed(@NonNull Context context, @NonNull String task, long delayMillis, @DrawableRes int iconRes) {
Preconditions.checkArgument(Build.VERSION.SDK_INT < 31);
return DelayedNotificationController.create(delayMillis, () -> {
try {
return startForegroundTask(context, task, DEFAULTS.channelId, iconRes);
} catch (UnableToStartException e) {
Log.w(TAG, "This should not happen on API < 31", e);
throw new AssertionError(e.getCause());
}
});
}
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task) throws UnableToStartException {
return startForegroundTask(context, task, DEFAULTS.channelId);
}
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task, @NonNull String channelId)
throws UnableToStartException
{
return startForegroundTask(context, task, channelId, DEFAULTS.iconRes);
}
public static NotificationController startForegroundTask(
@NonNull Context context,
@NonNull String task,
@NonNull String channelId,
@DrawableRes int iconRes)
throws UnableToStartException
{
final int id = NEXT_ID.getAndIncrement();
Intent intent = new Intent(context, GenericForegroundService.class);
intent.setAction(ACTION_START);
intent.putExtra(EXTRA_TITLE, task);
intent.putExtra(EXTRA_CHANNEL_ID, channelId);
intent.putExtra(EXTRA_ICON_RES, iconRes);
intent.putExtra(EXTRA_ID, id);
Log.i(TAG, String.format(Locale.US, "Starting foreground service (%s) id=%d", task, id));
ForegroundServiceUtil.start(context, intent);
return new NotificationController(context, id);
}
public static void stopForegroundTask(@NonNull Context context, int id) {
Intent intent = new Intent(context, GenericForegroundService.class);
intent.setAction(ACTION_STOP);
intent.putExtra(EXTRA_ID, id);
Log.i(TAG, String.format(Locale.US, "Stopping foreground service id=%d", id));
ForegroundServiceUtil.startWhenCapableOrThrow(context, intent);
}
synchronized void replaceTitle(int id, @NonNull String title) {
Entry oldEntry = allActiveMessages.get(id);
if (oldEntry == null) {
Log.w(TAG, "Failed to replace notification, it was not found");
return;
}
Entry newEntry = new Entry(title, oldEntry.channelId, oldEntry.iconRes, oldEntry.id, oldEntry.progressMax, oldEntry.progress, oldEntry.indeterminate);
if (oldEntry.equals(newEntry)) {
Log.d(TAG, String.format("handleReplace() skip, no change %s", newEntry));
return;
}
Log.i(TAG, String.format("handleReplace() %s", newEntry));
allActiveMessages.put(newEntry.id, newEntry);
updateNotification();
}
synchronized void replaceProgress(int id, int progressMax, int progress, boolean indeterminate) {
Entry oldEntry = allActiveMessages.get(id);
if (oldEntry == null) {
Log.w(TAG, "Failed to replace notification, it was not found");
return;
}
Entry newEntry = new Entry(oldEntry.title, oldEntry.channelId, oldEntry.iconRes, oldEntry.id, progressMax, progress, indeterminate);
if (oldEntry.equals(newEntry)) {
Log.d(TAG, String.format("handleReplace() skip, no change %s", newEntry));
return;
}
Log.i(TAG, String.format("handleReplace() %s", newEntry));
allActiveMessages.put(newEntry.id, newEntry);
updateNotification();
}
private static class Entry {
final @NonNull String title;
final @NonNull String channelId;
final int id;
final @DrawableRes int iconRes;
final int progress;
final int progressMax;
final boolean indeterminate;
private Entry(@NonNull String title, @NonNull String channelId, @DrawableRes int iconRes, int id, int progressMax, int progress, boolean indeterminate) {
this.title = title;
this.channelId = channelId;
this.iconRes = iconRes;
this.id = id;
this.progress = progress;
this.progressMax = progressMax;
this.indeterminate = indeterminate;
}
private static Entry fromIntent(@NonNull Intent intent) {
int id = intent.getIntExtra(EXTRA_ID, DEFAULTS.id);
String title = intent.getStringExtra(EXTRA_TITLE);
if (title == null) title = DEFAULTS.title;
String channelId = intent.getStringExtra(EXTRA_CHANNEL_ID);
if (channelId == null) channelId = DEFAULTS.channelId;
int iconRes = intent.getIntExtra(EXTRA_ICON_RES, DEFAULTS.iconRes);
int progress = intent.getIntExtra(EXTRA_PROGRESS, DEFAULTS.progress);
int progressMax = intent.getIntExtra(EXTRA_PROGRESS_MAX, DEFAULTS.progressMax);
boolean indeterminate = intent.getBooleanExtra(EXTRA_PROGRESS_INDETERMINATE, DEFAULTS.indeterminate);
return new Entry(title, channelId, iconRes, id, progressMax, progress, indeterminate);
}
@Override
public @NonNull String toString() {
return String.format(Locale.US, "ChannelId: %s Id: %d Progress: %d/%d %s", channelId, id, progress, progressMax, indeterminate ? "indeterminate" : "determinate");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Entry entry = (Entry) o;
return id == entry.id &&
iconRes == entry.iconRes &&
progress == entry.progress &&
progressMax == entry.progressMax &&
indeterminate == entry.indeterminate &&
Objects.equals(title, entry.title) &&
Objects.equals(channelId, entry.channelId);
}
@Override
public int hashCode() {
int hashCode = title.hashCode();
hashCode *= 31;
hashCode += channelId.hashCode();
hashCode *= 31;
hashCode += id;
hashCode *= 31;
hashCode += iconRes;
hashCode *= 31;
hashCode += progress;
hashCode *= 31;
hashCode += progressMax;
hashCode *= 31;
hashCode += indeterminate ? 1 : 0;
return hashCode;
}
}
class LocalBinder extends Binder {
GenericForegroundService getService() {
// Return this instance of LocalService so clients can call public methods
return GenericForegroundService.this;
}
}
}