+ * With this, you can {@link #close()} it to dismiss or prevent it showing if it hasn't already. + *
+ * You can also {@link #showNow()} to show if it's not already. This returns a regular {@link NotificationController} which can be updated if required. + */ +public abstract class DelayedNotificationController implements AutoCloseable { + + private static final String TAG = Log.tag(DelayedNotificationController.class); + + public static final long SHOW_WITHOUT_DELAY = 0; + public static final long DO_NOT_SHOW = -1; + + private DelayedNotificationController() {} + + static DelayedNotificationController create(long delayMillis, @NonNull Create createTask) { + if (delayMillis == SHOW_WITHOUT_DELAY) return new Shown(createTask.create()); + if (delayMillis == DO_NOT_SHOW) return new NoShow(); + if (delayMillis > 0) return new DelayedShow(delayMillis, createTask); + + throw new IllegalArgumentException("Illegal delay " + delayMillis); + } + + /** + * Show the foreground notification if it's not already showing. + *
+ * If it does show, it returns a regular {@link NotificationController} which you can use to update its message or progress. + */ + public abstract @Nullable NotificationController showNow(); + + @Override + public void close() { + } + + private static final class NoShow extends DelayedNotificationController { + + @Override + public @Nullable NotificationController showNow() { + return null; + } + } + + private static final class Shown extends DelayedNotificationController { + + private final NotificationController controller; + + Shown(@NonNull NotificationController controller) { + this.controller = controller; + } + + @Override + public void close() { + this.controller.close(); + } + + @Override + public NotificationController showNow() { + return controller; + } + } + + private static final class DelayedShow extends DelayedNotificationController { + + private final Create createTask; + private final Handler handler; + private final Runnable start; + private NotificationController notificationController; + private boolean isClosed; + + private DelayedShow(long delayMillis, @NonNull Create createTask) { + this.createTask = createTask; + this.handler = new Handler(Looper.getMainLooper()); + this.start = this::start; + + handler.postDelayed(start, delayMillis); + } + + private void start() { + SignalExecutors.BOUNDED.execute(this::showNowInner); + } + + public synchronized @NonNull NotificationController showNow() { + if (isClosed) { + throw new AssertionError("showNow called after close"); + } + return Objects.requireNonNull(showNowInner()); + } + + private synchronized @Nullable NotificationController showNowInner() { + if (notificationController != null) { + return notificationController; + } + + if (!isClosed) { + Log.i(TAG, "Starting foreground service"); + notificationController = createTask.create(); + return notificationController; + } else { + Log.i(TAG, "Did not start foreground service as close has been called"); + return null; + } + } + + @Override + public synchronized void close() { + handler.removeCallbacks(start); + isClosed = true; + if (notificationController != null) { + Log.d(TAG, "Closing"); + notificationController.close(); + } else { + Log.d(TAG, "Never showed"); + } + } + } + + public interface Create { + @NonNull NotificationController create(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java index 69098fea8..568cd8c06 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java @@ -58,11 +58,14 @@ public final class GenericForegroundService extends Service { synchronized (GenericForegroundService.class) { String action = intent.getAction(); - 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(); + 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; } @@ -117,6 +120,15 @@ public final class GenericForegroundService extends Service { return binder; } + /** + * Waits for {@param delayMillis} ms before starting the foreground task. + *
+ * The delayed notification controller can also shown on demand and promoted to a regular notification controller to update the message etc.
+ */
+ public static DelayedNotificationController startForegroundTaskDelayed(@NonNull Context context, @NonNull String task, long delayMillis) {
+ return DelayedNotificationController.create(delayMillis, () -> startForegroundTask(context, task));
+ }
+
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task) {
return startForegroundTask(context, task, DEFAULTS.channelId);
}
@@ -135,6 +147,7 @@ public final class GenericForegroundService extends Service {
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));
ContextCompat.startForegroundService(context, intent);
return new NotificationController(context, id);
@@ -145,6 +158,7 @@ public final class GenericForegroundService extends Service {
intent.setAction(ACTION_STOP);
intent.putExtra(EXTRA_ID, id);
+ Log.i(TAG, String.format(Locale.US, "Stopping foreground service id=%d", id));
ContextCompat.startForegroundService(context, intent);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java b/app/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java
index 625a7261e..566964b8b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java
@@ -8,12 +8,17 @@ import android.os.IBinder;
import androidx.annotation.NonNull;
+import org.signal.core.util.logging.Log;
+
import java.util.concurrent.atomic.AtomicReference;
-public final class NotificationController implements AutoCloseable {
+public final class NotificationController implements AutoCloseable,
+ ServiceConnection
+{
+ private static final String TAG = Log.tag(NotificationController.class);
- private final @NonNull Context context;
- private final int id;
+ private final Context context;
+ private final int id;
private int progress;
private int progressMax;
@@ -30,22 +35,7 @@ public final class NotificationController implements AutoCloseable {
}
private void bindToService() {
- context.bindService(new Intent(context, GenericForegroundService.class), new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- GenericForegroundService.LocalBinder binder = (GenericForegroundService.LocalBinder) service;
- GenericForegroundService genericForegroundService = binder.getService();
-
- NotificationController.this.service.set(genericForegroundService);
-
- updateProgressOnService();
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- service.set(null);
- }
- }, Context.BIND_AUTO_CREATE);
+ context.bindService(new Intent(context, GenericForegroundService.class), this, Context.BIND_AUTO_CREATE);
}
public int getId() {
@@ -54,6 +44,7 @@ public final class NotificationController implements AutoCloseable {
@Override
public void close() {
+ context.unbindService(this);
GenericForegroundService.stopForegroundTask(context, id);
}
@@ -87,4 +78,23 @@ public final class NotificationController implements AutoCloseable {
genericForegroundService.replaceProgress(id, progressMax, progress, indeterminate);
}
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ Log.i(TAG, "Service connected " + name);
+
+ GenericForegroundService.LocalBinder binder = (GenericForegroundService.LocalBinder) service;
+ GenericForegroundService genericForegroundService = binder.getService();
+
+ this.service.set(genericForegroundService);
+
+ updateProgressOnService();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Log.i(TAG, "Service disconnected " + name);
+
+ service.set(null);
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java
index fbdb3f9a9..5aec76f97 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java
@@ -18,6 +18,8 @@ public abstract class PersistentAlarmManagerListener extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
+ Log.i(TAG, String.format("%s#onReceive(%s)", getClass().getSimpleName(), intent.getAction()));
+
long scheduledTime = getNextScheduledExecutionTime(context);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent(context, getClass());
@@ -27,7 +29,7 @@ public abstract class PersistentAlarmManagerListener extends BroadcastReceiver {
scheduledTime = onAlarm(context, scheduledTime);
}
- Log.i(TAG, getClass() + " scheduling for: " + scheduledTime);
+ Log.i(TAG, getClass() + " scheduling for: " + scheduledTime + " action: " + intent.getAction());
alarmManager.cancel(pendingIntent);
alarmManager.set(AlarmManager.RTC_WAKEUP, scheduledTime, pendingIntent);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
index 108212699..143f65c74 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.util;
import android.os.Build;
import android.text.TextUtils;
+import android.util.TimeUtils;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
+import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import java.util.HashMap;
import java.util.HashSet;
@@ -75,6 +77,7 @@ public final class FeatureFlags {
private static final String SHARE_SELECTION_LIMIT = "android.share.limit";
private static final String ANIMATED_STICKER_MIN_MEMORY = "android.animatedStickerMinMemory";
private static final String ANIMATED_STICKER_MIN_TOTAL_MEMORY = "android.animatedStickerMinTotalMemory";
+ private static final String MESSAGE_PROCESSOR_ALARM_INTERVAL = "android.messageProcessor.alarmIntervalMins";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -106,7 +109,8 @@ public final class FeatureFlags {
OKHTTP_AUTOMATIC_RETRY,
SHARE_SELECTION_LIMIT,
ANIMATED_STICKER_MIN_MEMORY,
- ANIMATED_STICKER_MIN_TOTAL_MEMORY
+ ANIMATED_STICKER_MIN_TOTAL_MEMORY,
+ MESSAGE_PROCESSOR_ALARM_INTERVAL
);
@VisibleForTesting
@@ -148,7 +152,8 @@ public final class FeatureFlags {
OKHTTP_AUTOMATIC_RETRY,
SHARE_SELECTION_LIMIT,
ANIMATED_STICKER_MIN_MEMORY,
- ANIMATED_STICKER_MIN_TOTAL_MEMORY
+ ANIMATED_STICKER_MIN_TOTAL_MEMORY,
+ MESSAGE_PROCESSOR_ALARM_INTERVAL
);
/**
@@ -171,6 +176,7 @@ public final class FeatureFlags {
* desired test state.
*/
private static final Map