mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-26 04:33:36 +00:00
Add foreground service when restoring backup media.
This commit is contained in:
@@ -70,6 +70,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getSerializableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.verify.VerifyBackupKeyActivity
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
@@ -127,6 +128,7 @@ import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment
|
||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment
|
||||
@@ -269,6 +271,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
}
|
||||
|
||||
if (SignalStore.backup.restoreState == RestoreState.RESTORING_MEDIA) {
|
||||
Log.i(TAG, "Still restoring media, launching a service.")
|
||||
BackupMediaRestoreService.resetTimeout()
|
||||
BackupMediaRestoreService.start(this, resources.getString(R.string.BackupStatus__restoring_media))
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, callback)
|
||||
|
||||
shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent)
|
||||
|
||||
@@ -121,6 +121,7 @@ import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
|
||||
import org.thoughtcrime.securesms.service.BackupProgressService
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
@@ -2127,6 +2128,7 @@ object BackupRepository {
|
||||
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_MEDIA
|
||||
|
||||
BackupMediaRestoreService.resetTimeout()
|
||||
AppDependencies.jobManager.add(BackupRestoreMediaJob())
|
||||
|
||||
Log.i(TAG, "[remoteRestore] Restore successful")
|
||||
@@ -2203,6 +2205,7 @@ object BackupRepository {
|
||||
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_MEDIA
|
||||
|
||||
BackupMediaRestoreService.resetTimeout()
|
||||
AppDependencies.jobManager.add(BackupRestoreMediaJob())
|
||||
|
||||
Log.i(TAG, "[restoreLinkAndSyncBackup] Restore successful")
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
@@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException
|
||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
@@ -117,6 +119,7 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo
|
||||
restoreFullAttachmentJobs.forEach { jobManager.add(it) }
|
||||
} while (restoreThumbnailJobs.isNotEmpty() || restoreFullAttachmentJobs.isNotEmpty() || notRestorable.isNotEmpty())
|
||||
|
||||
BackupMediaRestoreService.start(context, context.getString(R.string.BackupStatus__restoring_media))
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
|
||||
RestoreAttachmentJob.Queues.INITIAL_RESTORE.forEach { queue ->
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
@@ -49,6 +50,7 @@ class CheckRestoreMediaLeftJob private constructor(parameters: Parameters) : Job
|
||||
SignalStore.backup.totalRestorableAttachmentSize = 0
|
||||
SignalStore.backup.restoreState = RestoreState.NONE
|
||||
ArchiveRestoreProgress.onProcessEnd()
|
||||
BackupMediaRestoreService.stop(context)
|
||||
|
||||
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
|
||||
SignalStore.backup.deletionState = DeletionState.MEDIA_DOWNLOAD_FINISHED
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
|
||||
import org.thoughtcrime.securesms.mms.MmsException
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
||||
@@ -225,7 +226,18 @@ class RestoreAttachmentJob private constructor(
|
||||
}
|
||||
|
||||
SignalLocalMetrics.ArchiveAttachmentRestore.start(attachmentId)
|
||||
retrieveAttachment(messageId, attachmentId, attachment)
|
||||
|
||||
val progressServiceController = BackupMediaRestoreService.start(context, context.getString(R.string.BackupStatus__restoring_media))
|
||||
|
||||
if (progressServiceController != null) {
|
||||
progressServiceController.use {
|
||||
retrieveAttachment(messageId, attachmentId, attachment)
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Continuing without service.")
|
||||
retrieveAttachment(messageId, attachmentId, attachment)
|
||||
}
|
||||
|
||||
SignalLocalMetrics.ArchiveAttachmentRestore.end(attachmentId)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package org.thoughtcrime.securesms.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.PendingIntentFlags
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService.Companion.hasTimedOut
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import javax.annotation.CheckReturnValue
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* Foreground service used to track progress when restoring backup media.
|
||||
*
|
||||
* Like [AttachmentProgressService], this is allowed to be started by multiple people and
|
||||
* supports that by using a set of controllers instead of just one.
|
||||
*
|
||||
* Unlike other services, this can be started without opening the app which means that
|
||||
* it can crash if we hit the 6 hour data sync limit. To handle this, we keep track of [hasTimedOut]
|
||||
* that gets set in [onTimeout] and is reset every time we open the app / when the user initiates a backup restore.
|
||||
*
|
||||
* For the initial restoration of a backup (eg [RestoreState.PENDING] / [RestoreState.RESTORING_DB]), see [BackupProgressService] instead
|
||||
*/
|
||||
class BackupMediaRestoreService : SafeForegroundService() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BackupMediaRestoreService::class)
|
||||
|
||||
private var title: String = ""
|
||||
private var downloadedBytes = 0.bytes
|
||||
private var totalBytes = 0.bytes
|
||||
|
||||
private var hasTimedOut: Boolean = false
|
||||
private val listeners: MutableSet<() -> Unit> = CopyOnWriteArraySet()
|
||||
private val controllers: LinkedHashSet<Controller> = linkedSetOf()
|
||||
private val controllerLock = ReentrantLock()
|
||||
|
||||
@JvmStatic
|
||||
@CheckReturnValue
|
||||
fun start(context: Context, startingTitle: String): Controller? {
|
||||
controllerLock.withLock {
|
||||
val started = if (hasTimedOut) {
|
||||
Log.i(TAG, "[start] Service has hit the 6 hour time limit. Cannot start.")
|
||||
false
|
||||
} else if (controllers.isEmpty()) {
|
||||
Log.i(TAG, "[start] First controller. Starting.")
|
||||
SafeForegroundService.start(context, BackupMediaRestoreService::class.java)
|
||||
} else {
|
||||
Log.i(TAG, "[start] No need to start the service again. Already have an active controller.")
|
||||
true
|
||||
}
|
||||
|
||||
return if (started) {
|
||||
val controller = Controller(context, startingTitle)
|
||||
controllers += controller
|
||||
onControllersChanged(context)
|
||||
controller
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun stop(context: Context, fromTimeout: Boolean = false) {
|
||||
controllers.clear()
|
||||
stop(context, BackupMediaRestoreService::class.java, fromTimeout)
|
||||
}
|
||||
|
||||
fun resetTimeout() {
|
||||
Log.i(TAG, "Resetting the 6 hour timeout limit.")
|
||||
hasTimedOut = false
|
||||
}
|
||||
|
||||
private fun onControllersChanged(context: Context) {
|
||||
controllerLock.withLock {
|
||||
if (controllers.isNotEmpty()) {
|
||||
val originalTitle = title
|
||||
val originalDownloadedBytes = downloadedBytes
|
||||
val originalTotalBytes = totalBytes
|
||||
|
||||
title = controllers.first().title
|
||||
downloadedBytes = controllers.first().downloadedBytes
|
||||
totalBytes = controllers.first().totalBytes
|
||||
|
||||
if (originalTitle != title || originalDownloadedBytes != downloadedBytes || originalTotalBytes != totalBytes) {
|
||||
listeners.forEach { it() }
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "[onControllersChanged] No controllers remaining. Stopping.")
|
||||
stop(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getForegroundNotification(context: Context): Notification {
|
||||
val progress = (downloadedBytes.inWholeBytes.toFloat() / totalBytes.inWholeBytes).coerceIn(0f, 1f)
|
||||
val notificationBuilder = NotificationCompat.Builder(context, NotificationChannels.getInstance().OTHER)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(title)
|
||||
.setProgress(100, (progress * 100).toInt(), false)
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), PendingIntentFlags.mutable()))
|
||||
.setVibrate(longArrayOf(0))
|
||||
|
||||
if (downloadedBytes >= 0.bytes) {
|
||||
notificationBuilder.setContentText(context.resources.getString(R.string.BackupStatus__status_size_of_size, downloadedBytes.toUnitString(), totalBytes.toUnitString()))
|
||||
}
|
||||
|
||||
return notificationBuilder.build()
|
||||
}
|
||||
|
||||
val listener = {
|
||||
try {
|
||||
startForeground(notificationId, getForegroundNotification(Intent()))
|
||||
} catch (e: Exception) {
|
||||
if (Build.VERSION.SDK_INT >= 31 && e.message?.contains("Time limit", ignoreCase = true) == true) {
|
||||
Log.w(TAG, "Foreground service timed out, but not in onTimeout call", e)
|
||||
stopDueToTimeout()
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val tag: String = TAG
|
||||
override val notificationId: Int = NotificationIds.BACKUP_PROGRESS
|
||||
|
||||
override fun getForegroundNotification(intent: Intent): Notification {
|
||||
return getForegroundNotification(this)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
listeners += listener
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
listeners -= listener
|
||||
}
|
||||
|
||||
override fun onTimeout(startId: Int, fgsType: Int) {
|
||||
Log.w(TAG, "BackupProgressService has timed out. startId: $startId, foregroundServiceType: $fgsType")
|
||||
stopDueToTimeout()
|
||||
}
|
||||
|
||||
private fun stopDueToTimeout() {
|
||||
controllerLock.withLock {
|
||||
hasTimedOut = true
|
||||
controllers.forEach { it.closeFromTimeout() }
|
||||
stop(context = this, fromTimeout = true)
|
||||
}
|
||||
|
||||
listeners -= listener
|
||||
}
|
||||
|
||||
/**
|
||||
* Use to update notification progress/state.
|
||||
*/
|
||||
class Controller(private val context: Context, startingTitle: String) : AutoCloseable {
|
||||
|
||||
val title: String = startingTitle
|
||||
val downloadedBytes: ByteSize
|
||||
get() {
|
||||
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
return totalBytes - remainingAttachmentSize.bytes
|
||||
}
|
||||
val totalBytes: ByteSize
|
||||
get() = SignalStore.backup.totalRestorableAttachmentSize.bytes
|
||||
|
||||
fun closeFromTimeout() {
|
||||
controllerLock.withLock {
|
||||
controllers.remove(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
controllerLock.withLock {
|
||||
controllers.remove(this)
|
||||
onControllersChanged(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* Foreground service to provide "long" run support to backup jobs.
|
||||
*
|
||||
* For the restoration of backup media, see [BackupMediaRestoreService] instead
|
||||
*
|
||||
*/
|
||||
class BackupProgressService : SafeForegroundService() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user