Signal-Android/app/src/main/java/org/thoughtcrime/securesms/jobs/ForegroundServiceUtil.kt

167 wiersze
5.9 KiB
Kotlin

package org.thoughtcrime.securesms.jobs
import android.app.AlarmManager
import android.app.ForegroundServiceStartNotAllowedException
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.SystemClock
import androidx.core.content.ContextCompat
import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.service.GenericForegroundService
import org.thoughtcrime.securesms.service.NotificationController
import org.thoughtcrime.securesms.util.ServiceUtil
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.time.Duration.Companion.minutes
/**
* Helps start foreground services from the background.
*/
object ForegroundServiceUtil {
private val TAG = Log.tag(ForegroundServiceUtil::class.java)
private val updateMutex: ReentrantLock = ReentrantLock()
private var activeLatch: CountDownLatch? = null
private val DEFAULT_TIMEOUT: Long = 1.minutes.inWholeMilliseconds
/**
* A simple wrapper around [ContextCompat.startForegroundService], but makes the possible failure part of the contract by forcing the caller to handle the
* [UnableToStartException].
*/
@JvmStatic
@Throws(UnableToStartException::class)
fun start(context: Context, intent: Intent) {
if (Build.VERSION.SDK_INT < 31) {
ContextCompat.startForegroundService(context, intent)
} else {
try {
ContextCompat.startForegroundService(context, intent)
} catch (e: IllegalStateException) {
if (e is ForegroundServiceStartNotAllowedException) {
Log.e(TAG, "Unable to start foreground service", e)
throw UnableToStartException(e)
} else {
throw e
}
}
}
}
/**
* Literally just a wrapper around [ContextCompat.startForegroundService], but with a name to make the caller understand that this method could throw.
*/
@JvmStatic
fun startOrThrow(context: Context, intent: Intent) {
ContextCompat.startForegroundService(context, intent)
}
/**
* Does its best to start a foreground service, including possibly blocking and waiting until we are able to.
* However, it is always possible that the attempt will fail, so be sure to handle the [UnableToStartException].
*
* @param timeout The maximum time you're willing to wait to create the conditions for a foreground service to start.
*/
@JvmOverloads
@JvmStatic
@Throws(UnableToStartException::class)
fun startWhenCapable(context: Context, intent: Intent, timeout: Long = DEFAULT_TIMEOUT) {
try {
start(context, intent)
} catch (e: UnableToStartException) {
Log.w(TAG, "Failed to start normally. Blocking and then trying again.")
blockUntilCapable(context, timeout)
start(context, intent)
}
}
/**
* Identical to [startWhenCapable], except in this case we just throw if we're unable to start the service. Should only be used if there's no good way
* to gracefully handle the exception (or if you know for sure it won't get thrown).
*
* @param timeout The maximum time you're willing to wait to create the conditions for a foreground service to start.
*/
@JvmOverloads
@JvmStatic
fun startWhenCapableOrThrow(context: Context, intent: Intent, timeout: Long = DEFAULT_TIMEOUT) {
try {
startWhenCapable(context, intent, timeout)
} catch (e: UnableToStartException) {
throw IllegalStateException(e)
}
}
/**
* Does its best to start a foreground service with your task name, including possibly blocking and waiting until we are able to.
* However, it is always possible that the attempt will fail, so always handle the [UnableToStartException].
*/
@Throws(UnableToStartException::class)
@JvmStatic
fun startGenericTaskWhenCapable(context: Context, task: String): NotificationController {
blockUntilCapable(context)
return GenericForegroundService.startForegroundTask(context, task)
}
/**
* Waits until we're capable of starting a foreground service.
* @return True if you should expect to be able to start a foreground service, otherwise false. Please note that this isn't *guaranteed*, just what we believe
* given the information we have.
*/
private fun blockUntilCapable(context: Context, timeout: Long = DEFAULT_TIMEOUT): Boolean {
val alarmManager = ServiceUtil.getAlarmManager(context)
if (Build.VERSION.SDK_INT < 31 || ApplicationDependencies.getAppForegroundObserver().isForegrounded) {
return true
}
if (!alarmManager.canScheduleExactAlarms()) {
return false
}
val latch: CountDownLatch? = updateMutex.withLock {
if (activeLatch == null) {
if (alarmManager.canScheduleExactAlarms()) {
activeLatch = CountDownLatch(1)
val pendingIntent = PendingIntent.getBroadcast(context, 0, Intent(context, Receiver::class.java), PendingIntentFlags.mutable())
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 1000, pendingIntent)
} else {
Log.w(TAG, "Unable to schedule alarm")
}
}
activeLatch
}
if (latch != null) {
try {
if (!latch.await(timeout, TimeUnit.MILLISECONDS)) {
Log.w(TAG, "Time ran out waiting for foreground")
return false
}
} catch (e: InterruptedException) {
Log.w(TAG, "Interrupted while waiting for foreground")
}
}
return true
}
class Receiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
updateMutex.withLock {
activeLatch?.countDown()
activeLatch = null
}
}
}
}
class UnableToStartException(cause: Throwable) : Exception(cause)