mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 00:59:49 +01:00
Add CallingAssetsDownloadJob to app startup to init calling assets
Co-authored-by: Greyson Parrelli <greyson@signal.org>
This commit is contained in:
committed by
Greyson Parrelli
parent
8ea90c8a43
commit
f1ebd2dc81
@@ -62,6 +62,7 @@ import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
|
||||
import org.thoughtcrime.securesms.jobs.BackupRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.BackupSubscriptionCheckJob;
|
||||
import org.thoughtcrime.securesms.jobs.BuildExpirationConfirmationJob;
|
||||
import org.thoughtcrime.securesms.jobs.CallingAssetsDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.CheckKeyTransparencyJob;
|
||||
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
|
||||
@@ -102,6 +103,7 @@ import org.thoughtcrime.securesms.service.MessageBackupListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.service.webrtc.ActiveCallManager;
|
||||
import org.thoughtcrime.securesms.service.webrtc.CallingAssets;
|
||||
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
@@ -226,6 +228,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
.addPostRender(RetrieveRemoteAnnouncementsJob::enqueue)
|
||||
.addPostRender(AndroidTelecomUtil::registerPhoneAccount)
|
||||
.addPostRender(() -> AppDependencies.getJobManager().add(new FontDownloaderJob()))
|
||||
.addPostRender(() -> AppDependencies.getJobManager().add(new CallingAssetsDownloadJob()))
|
||||
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
|
||||
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
|
||||
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.AutoDownloadEmojiConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.service.webrtc.CallingAssets
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
/**
|
||||
* Job that downloads missing calling assets.
|
||||
*/
|
||||
class CallingAssetsDownloadJob private constructor(parameters: Parameters) : Job(parameters) {
|
||||
companion object {
|
||||
private val TAG = Log.tag(CallingAssetsDownloadJob::class)
|
||||
|
||||
const val KEY = "CallingAssetsDownloadJob"
|
||||
}
|
||||
|
||||
constructor() : this(
|
||||
Parameters.Builder()
|
||||
.addConstraint(AutoDownloadEmojiConstraint.KEY)
|
||||
.setLifespan(3.days.inWholeMilliseconds)
|
||||
.setMaxAttempts(5)
|
||||
.setMaxInstancesForFactory(1)
|
||||
.build()
|
||||
)
|
||||
|
||||
override fun serialize(): ByteArray? = null
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun run(): Result {
|
||||
var succeeded = true
|
||||
if (SignalStore.misc.callingAssetsVersion != CallingAssets.CURRENT_VERSION) {
|
||||
succeeded = CallingAssets.downloadMissingAssets()
|
||||
}
|
||||
|
||||
CallingAssets.registerAssetsIfNeeded()
|
||||
|
||||
if (!succeeded) {
|
||||
Log.w(TAG, "Failed to download some calling assets")
|
||||
return Result.retry(BackoffUtil.exponentialBackoff(runAttempt + 1, 1.hours.inWholeMilliseconds))
|
||||
}
|
||||
SignalStore.misc.callingAssetsVersion = CallingAssets.CURRENT_VERSION
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
class Factory : Job.Factory<CallingAssetsDownloadJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): CallingAssetsDownloadJob {
|
||||
return CallingAssetsDownloadJob(parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,7 @@ public final class JobManagerFactories {
|
||||
put(BackupRestoreMediaJob.KEY, new BackupRestoreMediaJob.Factory());
|
||||
put(BackupSubscriptionCheckJob.KEY, new BackupSubscriptionCheckJob.Factory());
|
||||
put(BuildExpirationConfirmationJob.KEY, new BuildExpirationConfirmationJob.Factory());
|
||||
put(CallingAssetsDownloadJob.KEY, new CallingAssetsDownloadJob.Factory());
|
||||
put(CallLinkPeekJob.KEY, new CallLinkPeekJob.Factory());
|
||||
put(CallLinkUpdateSendJob.KEY, new CallLinkUpdateSendJob.Factory());
|
||||
put(CallLogEventSendJob.KEY, new CallLogEventSendJob.Factory());
|
||||
|
||||
@@ -47,8 +47,9 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
|
||||
private const val HAS_KEY_TRANSPARENCY_FAILURE = "misc.has_key_transparency_failure"
|
||||
private const val HAS_SEEN_KEY_TRANSPARENCY_FAILURE = "misc.has_seen_key_transparency_failure"
|
||||
private const val CAMERA_FACING_FRONT = "misc.camera_facing_front"
|
||||
private const val CAPTCHA_LAST_VIEWED_AT = "misc.captcha_last_viewed_at"
|
||||
private const val COMPLETED_COLLAPSED_EVENTS_MIGRATION = "misc.completed_collapsed_events_migration"
|
||||
private const val CAPTCHA_LAST_VIEWED_AT = "misc.captcha_last_viewed_at"
|
||||
private const val CALLING_ASSETS_VERSION = "misc.calling_assets_version"
|
||||
}
|
||||
|
||||
public override fun onFirstEverAppLaunch() {
|
||||
@@ -324,4 +325,11 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
|
||||
* The last time the user viewed the captcha/recaptcha proof activity.
|
||||
*/
|
||||
var captchaLastViewedAt: Long by longValue(CAPTCHA_LAST_VIEWED_AT, 0)
|
||||
|
||||
/**
|
||||
* The last successfully-downloaded calling assets version. Compared against
|
||||
* [org.thoughtcrime.securesms.service.webrtc.CallingAssets.CURRENT_VERSION] to determine
|
||||
* if new assets need to be fetched.
|
||||
*/
|
||||
var callingAssetsVersion: Int by integerValue(CALLING_ASSETS_VERSION, 0)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import okio.IOException
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.s3.S3
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.security.MessageDigest
|
||||
import java.util.Base64
|
||||
|
||||
/**
|
||||
* Manages downloading and registering calling assets (e.g. DRED weights).
|
||||
*/
|
||||
object CallingAssets {
|
||||
private val TAG = Log.tag(CallingAssets::class)
|
||||
|
||||
private const val BASE_DIRECTORY = "calling-assets"
|
||||
|
||||
/** Increment this whenever an asset is added, removed, or updated. */
|
||||
const val CURRENT_VERSION = 1
|
||||
|
||||
private val ASSETS: List<ManifestEntry> = listOf(
|
||||
ManifestEntry(
|
||||
assetGroup = "opus-dred",
|
||||
name = "calling-dred_weights-1_6_1-f4aed08a.bin",
|
||||
digest = "sdfpdb/u3wiTfBr2s0gx1LJX6jii4tquyax/UBThTGWTEXyOCSKjYmYV+9tKQZcO+Q1B1ReoGSW3VbvzeMGKaQ==",
|
||||
url = "https://updates2.signal.org/static/android/calling/deep_plc-dred_weights-1_6_1-f4aed08a.bin",
|
||||
size = 1998208
|
||||
)
|
||||
)
|
||||
|
||||
private val registeredLog = HashSet<String>(ASSETS.size)
|
||||
|
||||
/**
|
||||
* Registers any downloaded assets with the call manager that haven't been registered yet this session.
|
||||
* Safe to call multiple times -- assets already registered are skipped.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun registerAssetsIfNeeded() {
|
||||
ASSETS.forEach { entry ->
|
||||
if (registeredLog.contains(entry.name)) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
try {
|
||||
val content = getFromFile(entry.name) ?: return@forEach
|
||||
if (verify(content, entry)) {
|
||||
AppDependencies.signalCallManager.addAsset(entry.assetGroup, content)
|
||||
registeredLog.add(entry.name)
|
||||
Log.i(TAG, "Registered calling asset: ${entry.name}")
|
||||
} else {
|
||||
Log.w(TAG, "Invalid calling asset on disk, skipping registration: ${entry.name}")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to register calling asset ${entry.name}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads any assets not yet present on disk.
|
||||
* @return true if all assets are present on disk after this call.
|
||||
*/
|
||||
fun downloadMissingAssets(): Boolean {
|
||||
var allDownloaded = true
|
||||
|
||||
ASSETS.forEach { entry ->
|
||||
try {
|
||||
val dataOnDisk = getFromFile(entry.name)
|
||||
if (dataOnDisk != null) {
|
||||
if (verify(dataOnDisk, entry)) {
|
||||
Log.i(TAG, "Calling asset already on disk: ${entry.name}")
|
||||
return@forEach
|
||||
} else {
|
||||
Log.w(TAG, "Invalid calling asset found on disk: ${entry.name}")
|
||||
}
|
||||
}
|
||||
|
||||
val remoteData = getFromRemote(entry.url)
|
||||
if (remoteData != null) {
|
||||
if (verify(remoteData, entry)) {
|
||||
Log.i(TAG, "Calling asset successfully downloaded: ${entry.name}")
|
||||
val dir = AppDependencies.application.getDir(BASE_DIRECTORY, Context.MODE_PRIVATE)
|
||||
File(dir, entry.name).writeBytes(remoteData)
|
||||
return@forEach
|
||||
} else {
|
||||
Log.w(TAG, "Failed to verify calling asset: ${entry.name}")
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "Unable to find or download calling asset ${entry.name}")
|
||||
allDownloaded = false
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unexpected exception while trying to find calling asset ${entry.name}", e)
|
||||
allDownloaded = false
|
||||
}
|
||||
}
|
||||
|
||||
if (allDownloaded) {
|
||||
deleteStaleAssets()
|
||||
}
|
||||
|
||||
return allDownloaded
|
||||
}
|
||||
|
||||
private fun getFromFile(assetName: String): ByteArray? {
|
||||
try {
|
||||
val file = File(AppDependencies.application.getDir(BASE_DIRECTORY, Context.MODE_PRIVATE), assetName)
|
||||
return if (file.exists()) file.readBytes() else null
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception while checking files for calling asset $assetName", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFromRemote(url: String): ByteArray? {
|
||||
try {
|
||||
val path = URI(url).path
|
||||
S3.getObject(path).use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw RuntimeException("Failed to download calling asset from $url: HTTP ${response.code}")
|
||||
}
|
||||
return response.body.bytes()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Exception while downloading calling asset from $url", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun verify(content: ByteArray, entry: ManifestEntry): Boolean {
|
||||
if (content.size != entry.size) {
|
||||
Log.w(TAG, "Unexpected size for calling asset ${entry.name}: expected=${entry.name},actual=${content.size}")
|
||||
return false
|
||||
}
|
||||
val hash = MessageDigest.getInstance("SHA-512").digest(content)
|
||||
val encodedHash = Base64.getEncoder().encodeToString(hash)
|
||||
return encodedHash == entry.digest
|
||||
}
|
||||
|
||||
private fun deleteStaleAssets() {
|
||||
try {
|
||||
val expectedNames = ASSETS.map { it.name }.toSet()
|
||||
val dir = AppDependencies.application.getDir(BASE_DIRECTORY, Context.MODE_PRIVATE)
|
||||
dir.listFiles()?.forEach { file ->
|
||||
if (file.name !in expectedNames) {
|
||||
Log.i(TAG, "Deleting stale calling asset: ${file.name}")
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to clean up stale calling assets", e)
|
||||
}
|
||||
}
|
||||
|
||||
data class ManifestEntry(
|
||||
val assetGroup: String,
|
||||
val name: String,
|
||||
val digest: String,
|
||||
val url: String,
|
||||
val size: Int
|
||||
)
|
||||
}
|
||||
@@ -1364,6 +1364,13 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
|
||||
return new SignalCallLinkManager(Objects.requireNonNull(callManager));
|
||||
}
|
||||
|
||||
public void addAsset(String assetGroup, byte[] content) throws CallException {
|
||||
if (callManager == null) {
|
||||
throw new CallException("Unable to add asset, call manager is not initialized");
|
||||
}
|
||||
callManager.addAsset(assetGroup, content);
|
||||
}
|
||||
|
||||
public void relaunchPipOnForeground() {
|
||||
AppForegroundObserver.addListener(new RelaunchListener(AppForegroundObserver.isForegrounded()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user