2022-05-11 18:33:54 +00:00
|
|
|
package org.thoughtcrime.securesms.jobs
|
|
|
|
|
|
|
|
import androidx.core.os.LocaleListCompat
|
|
|
|
import com.fasterxml.jackson.annotation.JsonProperty
|
|
|
|
import org.signal.core.util.Hex
|
|
|
|
import org.signal.core.util.ThreadUtil
|
|
|
|
import org.signal.core.util.logging.Log
|
|
|
|
import org.thoughtcrime.securesms.BuildConfig
|
|
|
|
import org.thoughtcrime.securesms.database.MessageDatabase
|
|
|
|
import org.thoughtcrime.securesms.database.RemoteMegaphoneDatabase
|
|
|
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
|
|
|
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
|
|
|
|
import org.thoughtcrime.securesms.database.model.addButton
|
|
|
|
import org.thoughtcrime.securesms.database.model.addLink
|
|
|
|
import org.thoughtcrime.securesms.database.model.addStyle
|
|
|
|
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
|
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|
|
|
import org.thoughtcrime.securesms.jobmanager.Data
|
|
|
|
import org.thoughtcrime.securesms.jobmanager.Job
|
|
|
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
|
|
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
2022-05-12 18:37:28 +00:00
|
|
|
import org.thoughtcrime.securesms.notifications.v2.ConversationId
|
2022-05-11 18:33:54 +00:00
|
|
|
import org.thoughtcrime.securesms.providers.BlobProvider
|
|
|
|
import org.thoughtcrime.securesms.recipients.Recipient
|
|
|
|
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
|
|
|
|
import org.thoughtcrime.securesms.s3.S3
|
|
|
|
import org.thoughtcrime.securesms.transport.RetryLaterException
|
|
|
|
import org.thoughtcrime.securesms.util.LocaleFeatureFlags
|
|
|
|
import org.whispersystems.signalservice.internal.ServiceResponse
|
|
|
|
import java.io.IOException
|
|
|
|
import java.lang.Integer.max
|
|
|
|
import java.security.MessageDigest
|
|
|
|
import java.util.Locale
|
|
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieves and processes release channel messages.
|
|
|
|
*/
|
|
|
|
class RetrieveRemoteAnnouncementsJob private constructor(private val force: Boolean, parameters: Parameters) : BaseJob(parameters) {
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
const val KEY = "RetrieveReleaseChannelJob"
|
2022-08-29 16:38:38 +00:00
|
|
|
private const val MANIFEST = "${S3.DYNAMIC_PATH}/release-notes/release-notes-v2.json"
|
2022-05-11 23:21:55 +00:00
|
|
|
private const val BASE_RELEASE_NOTE = "${S3.STATIC_PATH}/release-notes"
|
2022-05-11 18:33:54 +00:00
|
|
|
private const val KEY_FORCE = "force"
|
|
|
|
|
|
|
|
private val TAG = Log.tag(RetrieveRemoteAnnouncementsJob::class.java)
|
|
|
|
private val RETRIEVE_FREQUENCY = TimeUnit.DAYS.toMillis(3)
|
|
|
|
|
|
|
|
@JvmStatic
|
|
|
|
@JvmOverloads
|
|
|
|
fun enqueue(force: Boolean = false) {
|
|
|
|
if (!SignalStore.account().isRegistered) {
|
|
|
|
Log.i(TAG, "Not registered, skipping.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!force && System.currentTimeMillis() < SignalStore.releaseChannelValues().nextScheduledCheck) {
|
|
|
|
Log.i(TAG, "Too soon to check for updated release notes")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
val job = RetrieveRemoteAnnouncementsJob(
|
|
|
|
force,
|
|
|
|
Parameters.Builder()
|
|
|
|
.setQueue("RetrieveReleaseChannelJob")
|
|
|
|
.setMaxInstancesForFactory(1)
|
|
|
|
.setMaxAttempts(3)
|
|
|
|
.addConstraint(NetworkConstraint.KEY)
|
|
|
|
.build()
|
|
|
|
)
|
|
|
|
|
|
|
|
ApplicationDependencies.getJobManager()
|
|
|
|
.startChain(CreateReleaseChannelJob.create())
|
|
|
|
.then(job)
|
|
|
|
.enqueue()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun serialize(): Data = Data.Builder().putBoolean(KEY_FORCE, force).build()
|
|
|
|
|
|
|
|
override fun getFactoryKey(): String = KEY
|
|
|
|
|
|
|
|
override fun onFailure() = Unit
|
|
|
|
|
|
|
|
override fun onRun() {
|
|
|
|
if (!SignalStore.account().isRegistered) {
|
|
|
|
Log.i(TAG, "Not registered, skipping.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
val values = SignalStore.releaseChannelValues()
|
|
|
|
|
|
|
|
if (values.releaseChannelRecipientId == null) {
|
|
|
|
Log.w(TAG, "Release Channel recipient is null, this shouldn't happen, will try to create on next run")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!force && System.currentTimeMillis() < values.nextScheduledCheck) {
|
|
|
|
Log.i(TAG, "Too soon to check for updated release notes")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
val manifestMd5: ByteArray? = S3.getObjectMD5(MANIFEST)
|
|
|
|
|
|
|
|
if (manifestMd5 == null) {
|
|
|
|
Log.i(TAG, "Unable to retrieve manifest MD5")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
when {
|
|
|
|
values.highestVersionNoteReceived == 0 -> {
|
|
|
|
Log.i(TAG, "First check, saving code and skipping download")
|
|
|
|
values.highestVersionNoteReceived = BuildConfig.CANONICAL_VERSION_CODE
|
|
|
|
}
|
2022-06-22 17:54:07 +00:00
|
|
|
!force && MessageDigest.isEqual(manifestMd5, values.previousManifestMd5) -> {
|
2022-05-11 18:33:54 +00:00
|
|
|
Log.i(TAG, "Manifest has not changed since last fetch.")
|
|
|
|
}
|
|
|
|
else -> fetchManifest(manifestMd5)
|
|
|
|
}
|
|
|
|
|
|
|
|
values.previousManifestMd5 = manifestMd5
|
|
|
|
values.nextScheduledCheck = System.currentTimeMillis() + RETRIEVE_FREQUENCY
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun fetchManifest(manifestMd5: ByteArray) {
|
|
|
|
Log.i(TAG, "Updating release notes to ${Hex.toStringCondensed(manifestMd5)}")
|
|
|
|
|
|
|
|
val allReleaseNotes: ReleaseNotes? = S3.getAndVerifyObject(MANIFEST, ReleaseNotes::class.java, manifestMd5).result.orElse(null)
|
|
|
|
|
|
|
|
if (allReleaseNotes != null) {
|
|
|
|
updateReleaseNotes(allReleaseNotes.announcements)
|
|
|
|
updateMegaphones(allReleaseNotes.megaphones ?: emptyList())
|
|
|
|
} else {
|
|
|
|
Log.w(TAG, "Unable to retrieve manifest json")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Suppress("UsePropertyAccessSyntax")
|
|
|
|
private fun updateReleaseNotes(announcements: List<ReleaseNote>) {
|
|
|
|
val values = SignalStore.releaseChannelValues()
|
|
|
|
|
|
|
|
if (Recipient.resolved(values.releaseChannelRecipientId!!).isBlocked) {
|
|
|
|
Log.i(TAG, "Release channel is blocked, do not bother with updates")
|
|
|
|
values.highestVersionNoteReceived = announcements.mapNotNull { it.androidMinVersion?.toIntOrNull() }.maxOrNull() ?: values.highestVersionNoteReceived
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!values.hasMetConversationRequirement) {
|
|
|
|
if ((SignalDatabase.threads.getArchivedConversationListCount() + SignalDatabase.threads.getUnarchivedConversationListCount()) < 6) {
|
|
|
|
Log.i(TAG, "User does not have enough conversations to show release channel")
|
|
|
|
values.nextScheduledCheck = System.currentTimeMillis() + RETRIEVE_FREQUENCY
|
|
|
|
return
|
|
|
|
} else {
|
|
|
|
values.hasMetConversationRequirement = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
val resolvedNotes: List<FullReleaseNote?> = announcements
|
|
|
|
.asSequence()
|
|
|
|
.filter {
|
|
|
|
val minVersion = it.androidMinVersion?.toIntOrNull()
|
|
|
|
if (minVersion != null) {
|
|
|
|
minVersion > values.highestVersionNoteReceived && minVersion <= BuildConfig.CANONICAL_VERSION_CODE
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.filter { it.countries == null || LocaleFeatureFlags.shouldShowReleaseNote(it.uuid, it.countries) }
|
|
|
|
.sortedBy { it.androidMinVersion!!.toInt() }
|
|
|
|
.map { resolveReleaseNote(it) }
|
|
|
|
.toList()
|
|
|
|
|
|
|
|
if (resolvedNotes.any { it == null }) {
|
|
|
|
Log.w(TAG, "Some release notes did not resolve, aborting.")
|
|
|
|
throw RetryLaterException()
|
|
|
|
}
|
|
|
|
|
|
|
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(values.releaseChannelRecipientId!!))
|
|
|
|
var highestVersion = values.highestVersionNoteReceived
|
2022-06-22 17:54:07 +00:00
|
|
|
var addedNewNotes = false
|
2022-05-11 18:33:54 +00:00
|
|
|
|
|
|
|
resolvedNotes
|
|
|
|
.filterNotNull()
|
|
|
|
.forEach { note ->
|
|
|
|
val body = "${note.translation.title}\n\n${note.translation.body}"
|
|
|
|
val bodyRangeList = BodyRangeList.newBuilder()
|
|
|
|
.addStyle(BodyRangeList.BodyRange.Style.BOLD, 0, note.translation.title.length)
|
|
|
|
|
|
|
|
if (note.releaseNote.link?.isNotEmpty() == true && note.translation.linkText?.isNotEmpty() == true) {
|
|
|
|
val linkIndex = body.indexOf(note.translation.linkText)
|
|
|
|
if (linkIndex != -1 && linkIndex + note.translation.linkText.length < body.length) {
|
|
|
|
bodyRangeList.addLink(note.releaseNote.link, linkIndex, note.translation.linkText.length)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (note.releaseNote.ctaId?.isNotEmpty() == true && note.translation.callToActionText?.isNotEmpty() == true) {
|
|
|
|
bodyRangeList.addButton(note.translation.callToActionText, note.releaseNote.ctaId, body.lastIndex, 0)
|
|
|
|
}
|
|
|
|
|
2022-06-22 17:54:07 +00:00
|
|
|
ThreadUtil.sleep(5)
|
2022-07-01 17:29:50 +00:00
|
|
|
val insertResult: MessageDatabase.InsertResult? = ReleaseChannel.insertReleaseChannelMessage(
|
2022-05-11 18:33:54 +00:00
|
|
|
recipientId = values.releaseChannelRecipientId!!,
|
|
|
|
body = body,
|
|
|
|
threadId = threadId,
|
|
|
|
messageRanges = bodyRangeList.build(),
|
|
|
|
image = note.translation.image,
|
|
|
|
imageWidth = note.translation.imageWidth?.toIntOrNull() ?: 0,
|
|
|
|
imageHeight = note.translation.imageHeight?.toIntOrNull() ?: 0
|
|
|
|
)
|
|
|
|
|
|
|
|
if (insertResult != null) {
|
2022-08-12 16:27:01 +00:00
|
|
|
addedNewNotes = addedNewNotes || (note.releaseNote.includeBoostMessage ?: true)
|
2022-05-11 18:33:54 +00:00
|
|
|
SignalDatabase.attachments.getAttachmentsForMessage(insertResult.messageId)
|
|
|
|
.forEach { ApplicationDependencies.getJobManager().add(AttachmentDownloadJob(insertResult.messageId, it.attachmentId, false)) }
|
|
|
|
|
2022-05-12 18:37:28 +00:00
|
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.threadId))
|
2022-05-11 18:33:54 +00:00
|
|
|
TrimThreadJob.enqueueAsync(insertResult.threadId)
|
|
|
|
|
|
|
|
highestVersion = max(highestVersion, note.releaseNote.androidMinVersion!!.toInt())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-22 17:54:07 +00:00
|
|
|
if (addedNewNotes) {
|
|
|
|
ThreadUtil.sleep(5)
|
|
|
|
SignalDatabase.sms.insertBoostRequestMessage(values.releaseChannelRecipientId!!, threadId)
|
|
|
|
}
|
|
|
|
|
2022-05-11 18:33:54 +00:00
|
|
|
values.highestVersionNoteReceived = highestVersion
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateMegaphones(megaphones: List<RemoteMegaphone>) {
|
|
|
|
val resolvedMegaphones: List<FullRemoteMegaphone?> = megaphones
|
|
|
|
.asSequence()
|
|
|
|
.filter { it.androidMinVersion != null }
|
|
|
|
.map { resolveMegaphone(it) }
|
|
|
|
.toList()
|
|
|
|
|
|
|
|
if (resolvedMegaphones.any { it == null }) {
|
|
|
|
Log.w(TAG, "Some megaphones did not resolve, will retry later.")
|
|
|
|
throw RetryLaterException()
|
|
|
|
}
|
|
|
|
|
|
|
|
val manifestMegaphones: MutableSet<String> = mutableSetOf()
|
|
|
|
val existingMegaphones: Map<String, RemoteMegaphoneRecord> = SignalDatabase.remoteMegaphones.getAll().associateBy { it.uuid }
|
|
|
|
|
|
|
|
resolvedMegaphones
|
|
|
|
.filterNotNull()
|
|
|
|
.forEach { megaphone ->
|
|
|
|
val uuid = megaphone.remoteMegaphone.uuid
|
|
|
|
manifestMegaphones += uuid
|
|
|
|
if (existingMegaphones.contains(uuid)) {
|
|
|
|
SignalDatabase.remoteMegaphones.update(
|
|
|
|
uuid = uuid,
|
|
|
|
priority = megaphone.remoteMegaphone.priority,
|
|
|
|
countries = megaphone.remoteMegaphone.countries,
|
|
|
|
title = megaphone.translation.title,
|
|
|
|
body = megaphone.translation.body,
|
|
|
|
primaryActionText = megaphone.translation.primaryCtaText,
|
|
|
|
secondaryActionText = megaphone.translation.secondaryCtaText
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
val record = RemoteMegaphoneRecord(
|
|
|
|
uuid = uuid,
|
|
|
|
priority = megaphone.remoteMegaphone.priority,
|
|
|
|
countries = megaphone.remoteMegaphone.countries,
|
|
|
|
minimumVersion = megaphone.remoteMegaphone.androidMinVersion!!.toInt(),
|
|
|
|
doNotShowBefore = megaphone.remoteMegaphone.dontShowBeforeEpochSeconds?.let { TimeUnit.SECONDS.toMillis(it) } ?: 0,
|
|
|
|
doNotShowAfter = megaphone.remoteMegaphone.dontShowAfterEpochSeconds?.let { TimeUnit.SECONDS.toMillis(it) } ?: Long.MAX_VALUE,
|
|
|
|
showForNumberOfDays = megaphone.remoteMegaphone.showForNumberOfDays ?: 0,
|
|
|
|
conditionalId = megaphone.remoteMegaphone.conditionalId,
|
|
|
|
primaryActionId = RemoteMegaphoneRecord.ActionId.from(megaphone.remoteMegaphone.primaryCtaId),
|
|
|
|
secondaryActionId = RemoteMegaphoneRecord.ActionId.from(megaphone.remoteMegaphone.secondaryCtaId),
|
|
|
|
imageUrl = megaphone.translation.image,
|
|
|
|
title = megaphone.translation.title,
|
|
|
|
body = megaphone.translation.body,
|
|
|
|
primaryActionText = megaphone.translation.primaryCtaText,
|
|
|
|
secondaryActionText = megaphone.translation.secondaryCtaText
|
|
|
|
)
|
|
|
|
|
|
|
|
SignalDatabase.remoteMegaphones.insert(record)
|
|
|
|
|
|
|
|
if (record.imageUrl != null) {
|
|
|
|
ApplicationDependencies.getJobManager().add(FetchRemoteMegaphoneImageJob(record.uuid, record.imageUrl))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
val megaphonesToDelete = existingMegaphones
|
|
|
|
.filterKeys { !manifestMegaphones.contains(it) }
|
|
|
|
.filterValues { it.minimumVersion != RemoteMegaphoneDatabase.VERSION_FINISHED }
|
|
|
|
|
|
|
|
if (megaphonesToDelete.isNotEmpty()) {
|
|
|
|
Log.i(TAG, "Clearing ${megaphonesToDelete.size} stale megaphones ${megaphonesToDelete.keys}")
|
|
|
|
for ((uuid, megaphone) in megaphonesToDelete) {
|
|
|
|
if (megaphone.imageUri != null) {
|
|
|
|
BlobProvider.getInstance().delete(context, megaphone.imageUri)
|
|
|
|
}
|
|
|
|
SignalDatabase.remoteMegaphones.clear(uuid)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun resolveReleaseNote(releaseNote: ReleaseNote): FullReleaseNote? {
|
|
|
|
val potentialNoteUrls = "$BASE_RELEASE_NOTE/${releaseNote.uuid}".getLocaleUrls()
|
|
|
|
for (potentialUrl: String in potentialNoteUrls) {
|
|
|
|
val translationJson: ServiceResponse<TranslatedReleaseNote> = S3.getAndVerifyObject(potentialUrl, TranslatedReleaseNote::class.java)
|
|
|
|
|
|
|
|
if (translationJson.result.isPresent) {
|
|
|
|
return FullReleaseNote(releaseNote, translationJson.result.get())
|
|
|
|
} else if (translationJson.status != 404 && translationJson.executionError.orElse(null) !is S3.Md5FailureException) {
|
|
|
|
throw RetryLaterException()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun resolveMegaphone(remoteMegaphone: RemoteMegaphone): FullRemoteMegaphone? {
|
|
|
|
val potentialNoteUrls = "$BASE_RELEASE_NOTE/${remoteMegaphone.uuid}".getLocaleUrls()
|
|
|
|
for (potentialUrl: String in potentialNoteUrls) {
|
|
|
|
val translationJson: ServiceResponse<TranslatedRemoteMegaphone> = S3.getAndVerifyObject(potentialUrl, TranslatedRemoteMegaphone::class.java)
|
|
|
|
|
|
|
|
if (translationJson.result.isPresent) {
|
|
|
|
return FullRemoteMegaphone(remoteMegaphone, translationJson.result.get())
|
|
|
|
} else if (translationJson.status != 404 && translationJson.executionError.orElse(null) !is S3.Md5FailureException) {
|
|
|
|
throw RetryLaterException()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onShouldRetry(e: Exception): Boolean {
|
|
|
|
return e is RetryLaterException || e is IOException
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun String.getLocaleUrls(): List<String> {
|
|
|
|
val localeList: LocaleListCompat = LocaleListCompat.getDefault()
|
|
|
|
|
|
|
|
val potentialNoteUrls = mutableListOf<String>()
|
|
|
|
|
|
|
|
if (SignalStore.settings().language != "zz") {
|
|
|
|
potentialNoteUrls += "$this/${SignalStore.settings().language}.json"
|
|
|
|
}
|
|
|
|
|
|
|
|
for (index in 0 until localeList.size()) {
|
2022-09-13 14:16:01 +00:00
|
|
|
val locale: Locale = localeList.get(index) ?: continue
|
2022-05-11 18:33:54 +00:00
|
|
|
if (locale.language.isNotEmpty()) {
|
|
|
|
if (locale.country.isNotEmpty()) {
|
|
|
|
potentialNoteUrls += "$this/${locale.language}_${locale.country}.json"
|
|
|
|
}
|
|
|
|
potentialNoteUrls += "$this/${locale.language}.json"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
potentialNoteUrls += "$this/en.json"
|
|
|
|
|
|
|
|
return potentialNoteUrls
|
|
|
|
}
|
|
|
|
|
|
|
|
data class FullReleaseNote(val releaseNote: ReleaseNote, val translation: TranslatedReleaseNote)
|
|
|
|
|
|
|
|
data class FullRemoteMegaphone(val remoteMegaphone: RemoteMegaphone, val translation: TranslatedRemoteMegaphone)
|
|
|
|
|
|
|
|
data class ReleaseNotes(@JsonProperty val announcements: List<ReleaseNote>, @JsonProperty val megaphones: List<RemoteMegaphone>?)
|
|
|
|
|
|
|
|
data class ReleaseNote(
|
|
|
|
@JsonProperty val uuid: String,
|
|
|
|
@JsonProperty val countries: String?,
|
|
|
|
@JsonProperty val androidMinVersion: String?,
|
|
|
|
@JsonProperty val link: String?,
|
2022-08-12 16:27:01 +00:00
|
|
|
@JsonProperty val ctaId: String?,
|
|
|
|
@JsonProperty val includeBoostMessage: Boolean?
|
2022-05-11 18:33:54 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
data class RemoteMegaphone(
|
|
|
|
@JsonProperty val uuid: String,
|
|
|
|
@JsonProperty val priority: Long,
|
|
|
|
@JsonProperty val countries: String?,
|
|
|
|
@JsonProperty val androidMinVersion: String?,
|
|
|
|
@JsonProperty val dontShowBeforeEpochSeconds: Long?,
|
|
|
|
@JsonProperty val dontShowAfterEpochSeconds: Long?,
|
|
|
|
@JsonProperty val showForNumberOfDays: Long?,
|
|
|
|
@JsonProperty val conditionalId: String?,
|
|
|
|
@JsonProperty val primaryCtaId: String?,
|
|
|
|
@JsonProperty val secondaryCtaId: String?
|
|
|
|
)
|
|
|
|
|
|
|
|
data class TranslatedReleaseNote(
|
|
|
|
@JsonProperty val uuid: String,
|
|
|
|
@JsonProperty val image: String?,
|
|
|
|
@JsonProperty val imageWidth: String?,
|
|
|
|
@JsonProperty val imageHeight: String?,
|
|
|
|
@JsonProperty val linkText: String?,
|
|
|
|
@JsonProperty val title: String,
|
|
|
|
@JsonProperty val body: String,
|
|
|
|
@JsonProperty val callToActionText: String?,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class TranslatedRemoteMegaphone(
|
|
|
|
@JsonProperty val uuid: String,
|
|
|
|
@JsonProperty val image: String?,
|
|
|
|
@JsonProperty val title: String,
|
|
|
|
@JsonProperty val body: String,
|
|
|
|
@JsonProperty val primaryCtaText: String?,
|
|
|
|
@JsonProperty val secondaryCtaText: String?,
|
|
|
|
)
|
|
|
|
|
|
|
|
class Factory : Job.Factory<RetrieveRemoteAnnouncementsJob> {
|
|
|
|
override fun create(parameters: Parameters, data: Data): RetrieveRemoteAnnouncementsJob {
|
|
|
|
return RetrieveRemoteAnnouncementsJob(data.getBoolean(KEY_FORCE), parameters)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|