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