mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Add a new foreground service for attachment progress.
This commit is contained in:
@@ -1189,6 +1189,10 @@
|
||||
android:name=".service.GenericForegroundService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".service.AttachmentProgressService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".gcm.FcmFetchBackgroundService"
|
||||
android:exported="false"/>
|
||||
|
||||
@@ -31,7 +31,7 @@ import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality;
|
||||
import org.thoughtcrime.securesms.service.NotificationController;
|
||||
import org.thoughtcrime.securesms.service.AttachmentProgressService;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
@@ -205,9 +205,8 @@ public final class AttachmentCompressionJob extends BaseJob {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
try (NotificationController notification = ForegroundServiceUtil.startGenericTaskWhenCapable(context, context.getString(R.string.AttachmentUploadJob_compressing_video_start))) {
|
||||
|
||||
notification.setIndeterminateProgress();
|
||||
try (AttachmentProgressService.Controller notification = AttachmentProgressService.start(context, context.getString(R.string.AttachmentUploadJob_compressing_video_start))) {
|
||||
notification.setIndeterminate(true);
|
||||
|
||||
try (MediaDataSource dataSource = attachmentDatabase.mediaDataSourceFor(attachment.getAttachmentId(), false)) {
|
||||
if (dataSource == null) {
|
||||
@@ -233,7 +232,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
||||
try {
|
||||
try (OutputStream outputStream = ModernEncryptingPartOutputStream.createFor(attachmentSecret, file, true).second) {
|
||||
transcoder.transcode(percent -> {
|
||||
notification.setProgress(100, percent);
|
||||
notification.setProgress(percent / 100f);
|
||||
eventBus.postSticky(new PartProgressEvent(attachment,
|
||||
PartProgressEvent.Type.COMPRESSION,
|
||||
100,
|
||||
@@ -262,7 +261,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
||||
Log.i(TAG, "Compressing with android in-memory muxer");
|
||||
|
||||
try (MediaStream mediaStream = transcoder.transcode(percent -> {
|
||||
notification.setProgress(100, percent);
|
||||
notification.setProgress(percent/100f);
|
||||
eventBus.postSticky(new PartProgressEvent(attachment,
|
||||
PartProgressEvent.Type.COMPRESSION,
|
||||
100,
|
||||
|
||||
@@ -22,12 +22,11 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil.startGenericTaskWhenCapable
|
||||
import org.thoughtcrime.securesms.jobs.protos.AttachmentUploadJobData
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.NotificationController
|
||||
import org.thoughtcrime.securesms.service.AttachmentProgressService
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
|
||||
@@ -144,10 +143,10 @@ class AttachmentUploadJob private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAttachmentNotificationIfNeeded(attachment: Attachment): NotificationController? {
|
||||
private fun getAttachmentNotificationIfNeeded(attachment: Attachment): AttachmentProgressService.Controller? {
|
||||
return if (attachment.size >= FOREGROUND_LIMIT) {
|
||||
try {
|
||||
startGenericTaskWhenCapable(context, context.getString(R.string.AttachmentUploadJob_uploading_media))
|
||||
AttachmentProgressService.start(context, context.getString(R.string.AttachmentUploadJob_uploading_media))
|
||||
} catch (e: UnableToStartException) {
|
||||
Log.w(TAG, "Unable to start foreground service", e)
|
||||
null
|
||||
@@ -168,7 +167,7 @@ class AttachmentUploadJob private constructor(
|
||||
}
|
||||
|
||||
@Throws(InvalidAttachmentException::class)
|
||||
private fun buildAttachmentStream(attachment: Attachment, notification: NotificationController?, resumableUploadSpec: ResumableUpload): SignalServiceAttachmentStream {
|
||||
private fun buildAttachmentStream(attachment: Attachment, notification: AttachmentProgressService.Controller?, resumableUploadSpec: ResumableUpload): SignalServiceAttachmentStream {
|
||||
if (attachment.uri == null || attachment.size == 0L) {
|
||||
throw InvalidAttachmentException(IOException("Outgoing attachment has no data!"))
|
||||
}
|
||||
@@ -192,7 +191,7 @@ class AttachmentUploadJob private constructor(
|
||||
.withListener(object : SignalServiceAttachment.ProgressListener {
|
||||
override fun onAttachmentProgress(total: Long, progress: Long) {
|
||||
EventBus.getDefault().postSticky(PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress))
|
||||
notification?.setProgress(total, progress)
|
||||
notification?.progress = (progress.toFloat() / total)
|
||||
}
|
||||
|
||||
override fun shouldCancel(): Boolean {
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.notifications.v2.ConversationId;
|
||||
public final class NotificationIds {
|
||||
|
||||
public static final int FCM_FAILURE = 12;
|
||||
public static final int ATTACHMENT_PROGRESS = 50;
|
||||
public static final int APK_UPDATE_PROMPT_INSTALL = 666;
|
||||
public static final int APK_UPDATE_FAILED_INSTALL = 667;
|
||||
public static final int APK_UPDATE_SUCCESSFUL_INSTALL = 668;
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.UnableToStartException
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
/**
|
||||
* A service to show attachment progress. In order to ensure we only show one status notification,
|
||||
* this handles both compression progress and upload progress.
|
||||
*
|
||||
* This class has a bunch of stuff to allow multiple people to "start" this service, but write to a
|
||||
* single notification. That way if something is compressing while something else is uploading,
|
||||
* or if there's two things uploading, we just have the one notification.
|
||||
*
|
||||
* To do this, it maintains a set of controllers. The service is told when those controllers change,
|
||||
* and it will only render the oldest controller in the set.
|
||||
*
|
||||
* We also use the number of controllers to determine if we actually need to start/stop the actual service.
|
||||
*/
|
||||
class AttachmentProgressService : SafeForegroundService() {
|
||||
companion object {
|
||||
private val TAG = Log.tag(AttachmentProgressService::class.java)
|
||||
|
||||
private var title: String = ""
|
||||
private var progress: Float = 0f
|
||||
set(value) {
|
||||
field = value.coerceIn(0f, 1f)
|
||||
}
|
||||
private var indeterminate: Boolean = false
|
||||
|
||||
private val listeners: MutableSet<() -> Unit> = CopyOnWriteArraySet()
|
||||
private val controllers: LinkedHashSet<Controller> = linkedSetOf()
|
||||
private val controllerLock = ReentrantLock()
|
||||
|
||||
/**
|
||||
* Start the service with the provided [title]. You will receive a controllers that you can
|
||||
* use to update the notification.
|
||||
*
|
||||
* Important: This could fail to start! We do our best to start the service regardless of context,
|
||||
* but it will fail on some devices, throwing an [UnableToStartException] if it does so.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(UnableToStartException::class)
|
||||
fun start(context: Context, title: String): Controller {
|
||||
controllerLock.withLock {
|
||||
if (controllers.isEmpty()) {
|
||||
Log.i(TAG, "[start] First controller. Starting.")
|
||||
SafeForegroundService.start(context, AttachmentProgressService::class.java)
|
||||
} else {
|
||||
Log.i(TAG, "[start] No need to start the service again. Already have an active controller.")
|
||||
}
|
||||
|
||||
val controller = Controller(context, title)
|
||||
controllers += controller
|
||||
onControllersChanged(context)
|
||||
return controller
|
||||
}
|
||||
}
|
||||
|
||||
private fun stop(context: Context) {
|
||||
SafeForegroundService.stop(context, AttachmentProgressService::class.java)
|
||||
}
|
||||
|
||||
private fun onControllersChanged(context: Context) {
|
||||
controllerLock.withLock {
|
||||
if (controllers.isNotEmpty()) {
|
||||
val originalTitle = title
|
||||
val originalProgress = progress
|
||||
val originalIndeterminate = indeterminate
|
||||
|
||||
title = controllers.first().title
|
||||
progress = controllers.first().progress
|
||||
indeterminate = controllers.first().indeterminate
|
||||
|
||||
if (originalTitle != title || originalProgress != progress || originalIndeterminate != indeterminate) {
|
||||
listeners.forEach { it() }
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "[onControllersChanged] No controllers remaining. Stopping.")
|
||||
stop(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val listener = {
|
||||
startForeground(notificationId, getForegroundNotification(Intent()))
|
||||
}
|
||||
|
||||
override val tag: String = TAG
|
||||
|
||||
override val notificationId: Int = NotificationIds.ATTACHMENT_PROGRESS
|
||||
|
||||
override fun getForegroundNotification(intent: Intent): Notification {
|
||||
return NotificationCompat.Builder(this, NotificationChannels.getInstance().OTHER)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(title)
|
||||
.setProgress(100, (progress * 100).toInt(), indeterminate)
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, MainActivity.clearTop(this), PendingIntentFlags.mutable()))
|
||||
.setVibrate(longArrayOf(0))
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
listeners += listener
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
listeners -= listener
|
||||
}
|
||||
|
||||
class Controller(private val context: Context, title: String) : AutoCloseable {
|
||||
var title: String = title
|
||||
set(value) {
|
||||
field = value
|
||||
onControllersChanged(context)
|
||||
}
|
||||
|
||||
var progress: Float = 0f
|
||||
set(value) {
|
||||
field = value
|
||||
indeterminate = false
|
||||
onControllersChanged(context)
|
||||
}
|
||||
|
||||
var indeterminate: Boolean = false
|
||||
private set
|
||||
|
||||
/** Has to have separate setter to avoid infinite loops when [progress] and [indeterminate] interact. */
|
||||
fun setIndeterminate(value: Boolean) {
|
||||
indeterminate = value
|
||||
progress = 0f
|
||||
onControllersChanged(context)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
controllerLock.withLock {
|
||||
controllers.remove(this)
|
||||
onControllersChanged(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.ServiceCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil
|
||||
import org.thoughtcrime.securesms.jobs.UnableToStartException
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
/**
|
||||
* A simple parent class meant to encourage the safe usage of foreground services.
|
||||
* Specifically, it ensures that both starting and "stopping" are done through
|
||||
* service starts and that we _always_ post a foreground notification.
|
||||
*/
|
||||
abstract class SafeForegroundService : Service() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(SafeForegroundService::class.java)
|
||||
|
||||
private const val ACTION_START = "start"
|
||||
private const val ACTION_STOP = "stop"
|
||||
|
||||
/**
|
||||
* Safety starts the target foreground service.
|
||||
* Important: This operation can fail. If it does, [UnableToStartException] is thrown.
|
||||
*/
|
||||
@Throws(UnableToStartException::class)
|
||||
fun start(context: Context, serviceClass: Class<out SafeForegroundService>) {
|
||||
val intent = Intent(context, serviceClass).apply {
|
||||
action = ACTION_START
|
||||
}
|
||||
|
||||
ForegroundServiceUtil.startWhenCapable(context, intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely stops the service by starting it with an action to stop itself.
|
||||
* This is done to prevent scenarios where you stop the service while
|
||||
* a start is pending, preventing the posting of a foreground notification.
|
||||
*/
|
||||
fun stop(context: Context, serviceClass: Class<out SafeForegroundService>) {
|
||||
val intent = Intent(context, serviceClass).apply {
|
||||
action = ACTION_STOP
|
||||
}
|
||||
|
||||
try {
|
||||
ForegroundServiceUtil.startWhenCapable(context, intent)
|
||||
} catch (e: UnableToStartException) {
|
||||
Log.w(TAG, "Failed to start service class $serviceClass", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
Log.d(tag, "[onCreate]")
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
checkNotNull(intent) { "Must have an intent!" }
|
||||
|
||||
Log.d(tag, "[onStartCommand] action: ${intent.action}")
|
||||
|
||||
startForeground(notificationId, getForegroundNotification(intent))
|
||||
|
||||
when (val action = intent.action) {
|
||||
ACTION_START -> {
|
||||
onServiceStarted(intent)
|
||||
}
|
||||
ACTION_STOP -> {
|
||||
onServiceStopped(intent)
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
else -> Log.w(tag, "Unknown action: $action")
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(tag, "[onDestroy]")
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
/** Log tag for improved logging */
|
||||
abstract val tag: String
|
||||
|
||||
/** Notification ID to use when posting the foreground notification */
|
||||
abstract val notificationId: Int
|
||||
|
||||
/** Notification to post as our foreground notification. */
|
||||
abstract fun getForegroundNotification(intent: Intent): Notification
|
||||
|
||||
/** Event listener for when the service is started via an intent. */
|
||||
open fun onServiceStarted(intent: Intent) = Unit
|
||||
|
||||
/** Event listener for when the service is stopped via an intent. */
|
||||
open fun onServiceStopped(intent: Intent) = Unit
|
||||
}
|
||||
Reference in New Issue
Block a user