diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java deleted file mode 100644 index 425e147058..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java +++ /dev/null @@ -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 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 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. - *

- * 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; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.kt new file mode 100644 index 0000000000..afa2318b6e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.kt @@ -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() + 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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java b/app/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java deleted file mode 100644 index 7416afd9b0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java +++ /dev/null @@ -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 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); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/NotificationController.kt b/app/src/main/java/org/thoughtcrime/securesms/service/NotificationController.kt new file mode 100644 index 0000000000..82b9bb7680 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/NotificationController.kt @@ -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() + 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!") + } +}