Convert GenericForegroundService to kotlin.
This commit is contained in:
parent
a911926119
commit
c69a4dda00
4 changed files with 395 additions and 435 deletions
|
@ -1,315 +0,0 @@
|
||||||
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 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.getInstance().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) throws UnableToStartException, IllegalStateException {
|
|
||||||
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.startWhenCapable(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,272 @@
|
||||||
|
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.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
|
import org.signal.core.util.PendingIntentFlags.mutable
|
||||||
|
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.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
|
class GenericForegroundService : Service() {
|
||||||
|
private val binder: IBinder = LocalBinder()
|
||||||
|
private val allActiveMessages = LinkedHashMap<Int, Entry>()
|
||||||
|
private val lock = ReentrantLock()
|
||||||
|
|
||||||
|
private var lastPosted: Entry? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(GenericForegroundService::class.java)
|
||||||
|
|
||||||
|
private const val NOTIFICATION_ID = 827353982
|
||||||
|
private const val EXTRA_TITLE = "extra_title"
|
||||||
|
private const val EXTRA_CHANNEL_ID = "extra_channel_id"
|
||||||
|
private const val EXTRA_ICON_RES = "extra_icon_res"
|
||||||
|
private const val EXTRA_ID = "extra_id"
|
||||||
|
private const val EXTRA_PROGRESS = "extra_progress"
|
||||||
|
private const val EXTRA_PROGRESS_MAX = "extra_progress_max"
|
||||||
|
private const val EXTRA_PROGRESS_INDETERMINATE = "extra_progress_indeterminate"
|
||||||
|
private const val ACTION_START = "start"
|
||||||
|
private const val ACTION_STOP = "stop"
|
||||||
|
|
||||||
|
private val NEXT_ID = AtomicInteger()
|
||||||
|
private val DEFAULT_ENTRY = Entry("", NotificationChannels.getInstance().OTHER, R.drawable.ic_notification, -1, 0, 0, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Do not call this method on API > 31
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun startForegroundTaskDelayed(context: Context, task: String, delayMillis: Long, @DrawableRes iconRes: Int): DelayedNotificationController {
|
||||||
|
Preconditions.checkArgument(Build.VERSION.SDK_INT < 31)
|
||||||
|
Log.d(TAG, "[startForegroundTaskDelayed] Task: $task, Delay: $delayMillis")
|
||||||
|
|
||||||
|
return DelayedNotificationController.create(delayMillis) {
|
||||||
|
try {
|
||||||
|
return@create startForegroundTask(context, task, DEFAULT_ENTRY.channelId, iconRes)
|
||||||
|
} catch (e: UnableToStartException) {
|
||||||
|
Log.w(TAG, "This should not happen on API < 31", e)
|
||||||
|
throw AssertionError(e.cause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@JvmOverloads
|
||||||
|
@Throws(UnableToStartException::class)
|
||||||
|
fun startForegroundTask(
|
||||||
|
context: Context,
|
||||||
|
task: String,
|
||||||
|
channelId: String = DEFAULT_ENTRY.channelId,
|
||||||
|
@DrawableRes iconRes: Int = DEFAULT_ENTRY.iconRes
|
||||||
|
): NotificationController {
|
||||||
|
val id = NEXT_ID.getAndIncrement()
|
||||||
|
Log.i(TAG, "[startForegroundTask] Task: $task, ID: $id")
|
||||||
|
|
||||||
|
val intent = Intent(context, GenericForegroundService::class.java).apply {
|
||||||
|
action = ACTION_START
|
||||||
|
putExtra(EXTRA_TITLE, task)
|
||||||
|
putExtra(EXTRA_CHANNEL_ID, channelId)
|
||||||
|
putExtra(EXTRA_ICON_RES, iconRes)
|
||||||
|
putExtra(EXTRA_ID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
ForegroundServiceUtil.start(context, intent)
|
||||||
|
|
||||||
|
return NotificationController(context, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(UnableToStartException::class, IllegalStateException::class)
|
||||||
|
fun stopForegroundTask(context: Context, id: Int) {
|
||||||
|
Log.d(TAG, "[stopForegroundTask] ID: $id")
|
||||||
|
|
||||||
|
val intent = Intent(context, GenericForegroundService::class.java).apply {
|
||||||
|
action = ACTION_STOP
|
||||||
|
putExtra(EXTRA_ID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Stopping foreground service id=$id")
|
||||||
|
ForegroundServiceUtil.startWhenCapable(context, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
checkNotNull(intent) { "Intent needs to be non-null." }
|
||||||
|
Log.d(TAG, "[onStartCommand] Action: ${intent.action}")
|
||||||
|
|
||||||
|
lock.withLock {
|
||||||
|
when (val action = intent.action) {
|
||||||
|
ACTION_START -> {
|
||||||
|
val entry = Entry.fromIntent(intent)
|
||||||
|
Log.i(TAG, "[onStartCommand] Adding entry: $entry")
|
||||||
|
allActiveMessages[entry.id] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION_STOP -> {
|
||||||
|
val id = intent.getIntExtra(EXTRA_ID, -1)
|
||||||
|
val removed = allActiveMessages.remove(id)
|
||||||
|
if (removed != null) {
|
||||||
|
Log.i(TAG, "[onStartCommand] ID: $id, Removed: $removed")
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "[onStartCommand] Could not find entry to remove")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw IllegalStateException("Unexpected action: $action")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNotification()
|
||||||
|
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
Log.d(TAG, "[onCreate]")
|
||||||
|
super.onCreate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder {
|
||||||
|
Log.d(TAG, "[onBind]")
|
||||||
|
return binder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnbind(intent: Intent?): Boolean {
|
||||||
|
Log.d(TAG, "[onUnbind]")
|
||||||
|
return super.onUnbind(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRebind(intent: Intent?) {
|
||||||
|
Log.d(TAG, "[onRebind]")
|
||||||
|
super.onRebind(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
Log.d(TAG, "[onDestroy]")
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLowMemory() {
|
||||||
|
Log.d(TAG, "[onLowMemory]")
|
||||||
|
super.onLowMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTrimMemory(level: Int) {
|
||||||
|
Log.d(TAG, "[onTrimMemory] level: $level")
|
||||||
|
super.onTrimMemory(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceTitle(id: Int, title: String) {
|
||||||
|
lock.withLock {
|
||||||
|
updateEntry(id) { oldEntry ->
|
||||||
|
oldEntry.copy(title = title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceProgress(id: Int, progressMax: Int, progress: Int, indeterminate: Boolean) {
|
||||||
|
lock.withLock {
|
||||||
|
updateEntry(id) { oldEntry ->
|
||||||
|
oldEntry.copy(progressMax = progressMax, progress = progress, indeterminate = indeterminate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNotification() {
|
||||||
|
if (allActiveMessages.isNotEmpty()) {
|
||||||
|
postObligatoryForegroundNotification(allActiveMessages.values.first())
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Last request. Ending foreground service.")
|
||||||
|
postObligatoryForegroundNotification(lastPosted ?: DEFAULT_ENTRY)
|
||||||
|
|
||||||
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postObligatoryForegroundNotification(active: Entry) {
|
||||||
|
lastPosted = active
|
||||||
|
|
||||||
|
startForeground(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
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), mutable()))
|
||||||
|
.setVibrate(longArrayOf(0))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateEntry(id: Int, transform: (Entry) -> Entry) {
|
||||||
|
val oldEntry = allActiveMessages[id]
|
||||||
|
if (oldEntry == null) {
|
||||||
|
Log.w(TAG, "Failed to replace notification, it was not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val newEntry = transform(oldEntry)
|
||||||
|
|
||||||
|
if (oldEntry == newEntry) {
|
||||||
|
Log.d(TAG, "handleReplace() skip, no change $newEntry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "handleReplace() $newEntry")
|
||||||
|
allActiveMessages[newEntry.id] = newEntry
|
||||||
|
updateNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class Entry(
|
||||||
|
val title: String,
|
||||||
|
val channelId: String,
|
||||||
|
@field:DrawableRes @param:DrawableRes val iconRes: Int,
|
||||||
|
val id: Int,
|
||||||
|
val progressMax: Int,
|
||||||
|
val progress: Int,
|
||||||
|
val indeterminate: Boolean
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "ChannelId: $channelId, ID: $id, Progress: $progress/$progressMax ${if (indeterminate) "indeterminate" else "determinate"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromIntent(intent: Intent): Entry {
|
||||||
|
return Entry(
|
||||||
|
title = intent.getStringExtra(EXTRA_TITLE) ?: DEFAULT_ENTRY.title,
|
||||||
|
channelId = intent.getStringExtra(EXTRA_CHANNEL_ID) ?: DEFAULT_ENTRY.channelId,
|
||||||
|
iconRes = intent.getIntExtra(EXTRA_ICON_RES, DEFAULT_ENTRY.iconRes),
|
||||||
|
id = intent.getIntExtra(EXTRA_ID, DEFAULT_ENTRY.id),
|
||||||
|
progressMax = intent.getIntExtra(EXTRA_PROGRESS_MAX, DEFAULT_ENTRY.progressMax),
|
||||||
|
progress = intent.getIntExtra(EXTRA_PROGRESS, DEFAULT_ENTRY.progress),
|
||||||
|
indeterminate = intent.getBooleanExtra(EXTRA_PROGRESS_INDETERMINATE, DEFAULT_ENTRY.indeterminate)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class LocalBinder : Binder() {
|
||||||
|
val service: GenericForegroundService
|
||||||
|
get() = this@GenericForegroundService
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,120 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.service;
|
|
||||||
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.os.IBinder;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.jobs.UnableToStartException;
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
public final class NotificationController implements AutoCloseable,
|
|
||||||
ServiceConnection
|
|
||||||
{
|
|
||||||
private static final String TAG = Log.tag(NotificationController.class);
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final int id;
|
|
||||||
|
|
||||||
private int progress;
|
|
||||||
private int progressMax;
|
|
||||||
private boolean indeterminate;
|
|
||||||
private long percent = -1;
|
|
||||||
private boolean isBound;
|
|
||||||
|
|
||||||
private final AtomicReference<GenericForegroundService> service = new AtomicReference<>();
|
|
||||||
|
|
||||||
NotificationController(@NonNull Context context, int id) {
|
|
||||||
this.context = context;
|
|
||||||
this.id = id;
|
|
||||||
|
|
||||||
isBound = bindToService();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean bindToService() {
|
|
||||||
return context.bindService(new Intent(context, GenericForegroundService.class), this, Context.BIND_AUTO_CREATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
try {
|
|
||||||
if (isBound) {
|
|
||||||
context.unbindService(this);
|
|
||||||
isBound = false;
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Service was not bound at the time of close()...");
|
|
||||||
}
|
|
||||||
|
|
||||||
GenericForegroundService.stopForegroundTask(context, id);
|
|
||||||
} catch (IllegalStateException | UnableToStartException e) {
|
|
||||||
Log.w(TAG, "Failed to unbind service...", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIndeterminateProgress() {
|
|
||||||
setProgress(0, 0, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProgress(long newProgressMax, long newProgress) {
|
|
||||||
setProgress((int) newProgressMax, (int) newProgress, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void replaceTitle(@NonNull String title) {
|
|
||||||
GenericForegroundService genericForegroundService = service.get();
|
|
||||||
|
|
||||||
if (genericForegroundService == null) return;
|
|
||||||
|
|
||||||
genericForegroundService.replaceTitle(id, title);
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void setProgress(int newProgressMax, int newProgress, boolean indeterminant) {
|
|
||||||
int newPercent = newProgressMax != 0 ? 100 * newProgress / newProgressMax : -1;
|
|
||||||
|
|
||||||
boolean same = newPercent == percent && indeterminate == indeterminant;
|
|
||||||
|
|
||||||
percent = newPercent;
|
|
||||||
progress = newProgress;
|
|
||||||
progressMax = newProgressMax;
|
|
||||||
indeterminate = indeterminant;
|
|
||||||
|
|
||||||
if (same) return;
|
|
||||||
|
|
||||||
updateProgressOnService();
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void updateProgressOnService() {
|
|
||||||
GenericForegroundService genericForegroundService = service.get();
|
|
||||||
|
|
||||||
if (genericForegroundService == null) return;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
package org.thoughtcrime.securesms.service
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.IBinder
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.jobs.UnableToStartException
|
||||||
|
import org.thoughtcrime.securesms.service.GenericForegroundService.Companion.stopForegroundTask
|
||||||
|
import org.thoughtcrime.securesms.service.GenericForegroundService.LocalBinder
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
|
class NotificationController internal constructor(private val context: Context, val id: Int) : AutoCloseable, ServiceConnection {
|
||||||
|
private val service = AtomicReference<GenericForegroundService?>()
|
||||||
|
private val lock = ReentrantLock()
|
||||||
|
|
||||||
|
private var progress = 0
|
||||||
|
private var progressMax = 0
|
||||||
|
private var indeterminate = false
|
||||||
|
private var percent: Int = -1
|
||||||
|
private var isBound: Boolean
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(NotificationController::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
isBound = bindToService()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||||
|
Log.i(TAG, "[onServiceConnected] Name: $name")
|
||||||
|
|
||||||
|
val binder = service as LocalBinder
|
||||||
|
val genericForegroundService = binder.service
|
||||||
|
|
||||||
|
this.service.set(genericForegroundService)
|
||||||
|
|
||||||
|
lock.withLock {
|
||||||
|
updateProgressOnService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName) {
|
||||||
|
Log.i(TAG, "[onServiceDisconnected] Name: $name")
|
||||||
|
service.set(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
try {
|
||||||
|
if (isBound) {
|
||||||
|
Log.d(TAG, "[close] Unbinding service.")
|
||||||
|
context.unbindService(this)
|
||||||
|
isBound = false
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "[close] Service was not bound at the time of close()...")
|
||||||
|
}
|
||||||
|
stopForegroundTask(context, id)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
Log.w(TAG, "[close] Failed to unbind service...", e)
|
||||||
|
} catch (e: UnableToStartException) {
|
||||||
|
Log.w(TAG, "[close] Failed to unbind service...", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIndeterminateProgress() {
|
||||||
|
lock.withLock {
|
||||||
|
setProgress(
|
||||||
|
newProgressMax = 0,
|
||||||
|
newProgress = 0,
|
||||||
|
indeterminant = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setProgress(newProgressMax: Long, newProgress: Long) {
|
||||||
|
lock.withLock {
|
||||||
|
setProgress(
|
||||||
|
newProgressMax = newProgressMax.toInt(),
|
||||||
|
newProgress = newProgress.toInt(),
|
||||||
|
indeterminant = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceTitle(title: String) {
|
||||||
|
lock.withLock {
|
||||||
|
service.get()?.replaceTitle(id, title)
|
||||||
|
?: Log.w(TAG, "Tried to update the title, but the service was no longer bound!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindToService(): Boolean {
|
||||||
|
return context.bindService(Intent(context, GenericForegroundService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setProgress(newProgressMax: Int, newProgress: Int, indeterminant: Boolean) {
|
||||||
|
val newPercent = if (newProgressMax != 0) {
|
||||||
|
100 * newProgress / newProgressMax
|
||||||
|
} else {
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
|
||||||
|
val same = newPercent == percent && indeterminate == indeterminant
|
||||||
|
|
||||||
|
percent = newPercent
|
||||||
|
progress = newProgress
|
||||||
|
progressMax = newProgressMax
|
||||||
|
indeterminate = indeterminant
|
||||||
|
|
||||||
|
if (!same) {
|
||||||
|
updateProgressOnService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateProgressOnService() {
|
||||||
|
service.get()?.replaceProgress(id, progressMax, progress, indeterminate)
|
||||||
|
?: Log.w(TAG, "Tried to update the progress, but the service was no longer bound!")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue