Skip to content

Commit

Permalink
Add remote megaphone.
Browse files Browse the repository at this point in the history
  • Loading branch information
cody-signal authored and alex-signal committed May 12, 2022
1 parent 8202778 commit bb963f9
Show file tree
Hide file tree
Showing 20 changed files with 1,069 additions and 322 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveReleaseChannelJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
Expand Down Expand Up @@ -201,7 +201,7 @@ public void onCreate() {
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
.addPostRender(RetrieveReleaseChannelJob::enqueue)
.addPostRender(RetrieveRemoteAnnouncementsJob::enqueue)
.addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount())
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob
import org.thoughtcrime.securesms.jobs.RetrieveReleaseChannelJob
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
Expand Down Expand Up @@ -418,7 +418,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
title = DSLSettingsText.from(R.string.preferences__internal_fetch_release_channel),
onClick = {
SignalStore.releaseChannelValues().previousManifestMd5 = ByteArray(0)
RetrieveReleaseChannelJob.enqueue(force = true)
RetrieveRemoteAnnouncementsJob.enqueue(force = true)
}
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package org.thoughtcrime.securesms.database

import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import androidx.core.content.contentValuesOf
import androidx.core.net.toUri
import org.signal.core.util.readToList
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.signal.core.util.update
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
import java.util.concurrent.TimeUnit

/**
* Stores remotely configured megaphones.
*/
class RemoteMegaphoneDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) {

companion object {
private const val TABLE_NAME = "remote_megaphone"
private const val ID = "_id"
private const val UUID = "uuid"
private const val COUNTRIES = "countries"
private const val PRIORITY = "priority"
private const val MINIMUM_VERSION = "minimum_version"
private const val DONT_SHOW_BEFORE = "dont_show_before"
private const val DONT_SHOW_AFTER = "dont_show_after"
private const val SHOW_FOR_DAYS = "show_for_days"
private const val CONDITIONAL_ID = "conditional_id"
private const val PRIMARY_ACTION_ID = "primary_action_id"
private const val SECONDARY_ACTION_ID = "secondary_action_id"
private const val IMAGE_URL = "image_url"
private const val IMAGE_BLOB_URI = "image_uri"
private const val TITLE = "title"
private const val BODY = "body"
private const val PRIMARY_ACTION_TEXT = "primary_action_text"
private const val SECONDARY_ACTION_TEXT = "secondary_action_text"
private const val SHOWN_AT = "shown_at"
private const val FINISHED_AT = "finished_at"

val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$UUID TEXT UNIQUE NOT NULL,
$PRIORITY INTEGER NOT NULL,
$COUNTRIES TEXT,
$MINIMUM_VERSION INTEGER NOT NULL,
$DONT_SHOW_BEFORE INTEGER NOT NULL,
$DONT_SHOW_AFTER INTEGER NOT NULL,
$SHOW_FOR_DAYS INTEGER NOT NULL,
$CONDITIONAL_ID TEXT,
$PRIMARY_ACTION_ID TEXT,
$SECONDARY_ACTION_ID TEXT,
$IMAGE_URL TEXT,
$IMAGE_BLOB_URI TEXT DEFAULT NULL,
$TITLE TEXT NOT NULL,
$BODY TEXT NOT NULL,
$PRIMARY_ACTION_TEXT TEXT,
$SECONDARY_ACTION_TEXT TEXT,
$SHOWN_AT INTEGER DEFAULT 0,
$FINISHED_AT INTEGER DEFAULT 0
)
""".trimIndent()

const val VERSION_FINISHED = Int.MAX_VALUE
}

fun insert(record: RemoteMegaphoneRecord) {
writableDatabase.insert(TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, record.toContentValues())
}

fun update(uuid: String, priority: Long, countries: String?, title: String, body: String, primaryActionText: String?, secondaryActionText: String?) {
writableDatabase
.update(TABLE_NAME)
.values(
PRIORITY to priority,
COUNTRIES to countries,
TITLE to title,
BODY to body,
PRIMARY_ACTION_TEXT to primaryActionText,
SECONDARY_ACTION_TEXT to secondaryActionText
)
.where("$UUID = ?", uuid)
.run()
}

fun getAll(): List<RemoteMegaphoneRecord> {
return readableDatabase
.select()
.from(TABLE_NAME)
.run()
.readToList { it.toRemoteMegaphoneRecord() }
}

fun getPotentialMegaphonesAndClearOld(now: Long = System.currentTimeMillis()): List<RemoteMegaphoneRecord> {
val records: List<RemoteMegaphoneRecord> = readableDatabase
.select()
.from(TABLE_NAME)
.where("$FINISHED_AT = ? AND $MINIMUM_VERSION <= ? AND ($DONT_SHOW_AFTER > ? AND $DONT_SHOW_BEFORE < ?)", 0, BuildConfig.CANONICAL_VERSION_CODE, now, now)
.orderBy("$PRIORITY DESC")
.run()
.readToList { it.toRemoteMegaphoneRecord() }

val oldRecords: Set<RemoteMegaphoneRecord> = records
.filter { it.shownAt > 0 && it.showForNumberOfDays > 0 }
.filter { it.shownAt + TimeUnit.DAYS.toMillis(it.showForNumberOfDays) < now }
.toSet()

for (oldRecord in oldRecords) {
clear(oldRecord.uuid)
}

return records - oldRecords
}

fun setImageUri(uuid: String, uri: Uri?) {
writableDatabase
.update(TABLE_NAME)
.values(IMAGE_BLOB_URI to uri?.toString())
.where("$UUID = ?", uuid)
.run()
}

fun markShown(uuid: String) {
writableDatabase
.update(TABLE_NAME)
.values(SHOWN_AT to System.currentTimeMillis())
.where("$UUID = ?", uuid)
.run()
}

fun markFinished(uuid: String) {
writableDatabase
.update(TABLE_NAME)
.values(
IMAGE_URL to null,
IMAGE_BLOB_URI to null,
FINISHED_AT to System.currentTimeMillis()
)
.where("$UUID = ?", uuid)
.run()
}

fun clearImageUrl(uuid: String) {
writableDatabase
.update(TABLE_NAME)
.values(IMAGE_URL to null)
.where("$UUID = ?", uuid)
.run()
}

fun clear(uuid: String) {
writableDatabase
.update(TABLE_NAME)
.values(
MINIMUM_VERSION to VERSION_FINISHED,
IMAGE_URL to null,
IMAGE_BLOB_URI to null
)
.where("$UUID = ?", uuid)
.run()
}

private fun RemoteMegaphoneRecord.toContentValues(): ContentValues {
return contentValuesOf(
UUID to uuid,
PRIORITY to priority,
COUNTRIES to countries,
MINIMUM_VERSION to minimumVersion,
DONT_SHOW_BEFORE to doNotShowBefore,
DONT_SHOW_AFTER to doNotShowAfter,
SHOW_FOR_DAYS to showForNumberOfDays,
CONDITIONAL_ID to conditionalId,
PRIMARY_ACTION_ID to primaryActionId?.id,
SECONDARY_ACTION_ID to secondaryActionId?.id,
IMAGE_URL to imageUrl,
TITLE to title,
BODY to body,
PRIMARY_ACTION_TEXT to primaryActionText,
SECONDARY_ACTION_TEXT to secondaryActionText,
FINISHED_AT to finishedAt
)
}

private fun Cursor.toRemoteMegaphoneRecord(): RemoteMegaphoneRecord {
return RemoteMegaphoneRecord(
id = requireLong(ID),
uuid = requireNonNullString(UUID),
priority = requireLong(PRIORITY),
countries = requireString(COUNTRIES),
minimumVersion = requireInt(MINIMUM_VERSION),
doNotShowBefore = requireLong(DONT_SHOW_BEFORE),
doNotShowAfter = requireLong(DONT_SHOW_AFTER),
showForNumberOfDays = requireLong(SHOW_FOR_DAYS),
conditionalId = requireString(CONDITIONAL_ID),
primaryActionId = RemoteMegaphoneRecord.ActionId.from(requireString(PRIMARY_ACTION_ID)),
secondaryActionId = RemoteMegaphoneRecord.ActionId.from(requireString(SECONDARY_ACTION_ID)),
imageUrl = requireString(IMAGE_URL),
imageUri = requireString(IMAGE_BLOB_URI)?.toUri(),
title = requireNonNullString(TITLE),
body = requireNonNullString(BODY),
primaryActionText = requireString(PRIMARY_ACTION_TEXT),
secondaryActionText = requireString(SECONDARY_ACTION_TEXT),
shownAt = requireLong(SHOWN_AT),
finishedAt = requireLong(FINISHED_AT)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val distributionListDatabase: DistributionListDatabase = DistributionListDatabase(context, this)
val storySendsDatabase: StorySendsDatabase = StorySendsDatabase(context, this)
val cdsDatabase: CdsDatabase = CdsDatabase(context, this)
val remoteMegaphoneDatabase: RemoteMegaphoneDatabase = RemoteMegaphoneDatabase(context, this)

override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
db.enableWriteAheadLogging()
Expand Down Expand Up @@ -107,6 +108,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
db.execSQL(DonationReceiptDatabase.CREATE_TABLE)
db.execSQL(StorySendsDatabase.CREATE_TABLE)
db.execSQL(CdsDatabase.CREATE_TABLE)
db.execSQL(RemoteMegaphoneDatabase.CREATE_TABLE)
executeStatements(db, SearchDatabase.CREATE_TABLE)
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE)
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE)
Expand Down Expand Up @@ -495,5 +497,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
@get:JvmName("unknownStorageIds")
val unknownStorageIds: UnknownStorageIdDatabase
get() = instance!!.storageIdDatabase

@get:JvmStatic
@get:JvmName("remoteMegaphones")
val remoteMegaphones: RemoteMegaphoneDatabase
get() = instance!!.remoteMegaphoneDatabase
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,9 @@ object SignalDatabaseMigrations {
private const val STORY_SYNCS = 143
private const val GROUP_STORY_NOTIFICATIONS = 144
private const val GROUP_STORY_REPLY_CLEANUP = 145
private const val REMOTE_MEGAPHONE = 146

const val DATABASE_VERSION = 145
const val DATABASE_VERSION = 146

@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
Expand Down Expand Up @@ -2584,6 +2585,34 @@ object SignalDatabaseMigrations {
""".trimIndent()
)
}

if (oldVersion < REMOTE_MEGAPHONE) {
db.execSQL(
"""
CREATE TABLE remote_megaphone (
_id INTEGER PRIMARY KEY,
uuid TEXT UNIQUE NOT NULL,
priority INTEGER NOT NULL,
countries TEXT,
minimum_version INTEGER NOT NULL,
dont_show_before INTEGER NOT NULL,
dont_show_after INTEGER NOT NULL,
show_for_days INTEGER NOT NULL,
conditional_id TEXT,
primary_action_id TEXT,
secondary_action_id TEXT,
image_url TEXT,
image_uri TEXT DEFAULT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL,
primary_action_text TEXT,
secondary_action_text TEXT,
shown_at INTEGER DEFAULT 0,
finished_at INTEGER DEFAULT 0
)
"""
)
}
}

@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.database.model

import android.net.Uri

/**
* Represents a Remote Megaphone.
*/
data class RemoteMegaphoneRecord(
val id: Long = -1,
val priority: Long,
val uuid: String,
val countries: String?,
val minimumVersion: Int,
val doNotShowBefore: Long,
val doNotShowAfter: Long,
val showForNumberOfDays: Long,
val conditionalId: String?,
val primaryActionId: ActionId?,
val secondaryActionId: ActionId?,
val imageUrl: String?,
val imageUri: Uri? = null,
val title: String,
val body: String,
val primaryActionText: String?,
val secondaryActionText: String?,
val shownAt: Long = 0,
val finishedAt: Long = 0
) {
@get:JvmName("hasPrimaryAction")
val hasPrimaryAction = primaryActionId != null && primaryActionText != null

@get:JvmName("hasSecondaryAction")
val hasSecondaryAction = secondaryActionId != null && secondaryActionText != null

enum class ActionId(val id: String, val isDonateAction: Boolean = false) {
SNOOZE("snooze"),
FINISH("finish"),
DONATE("donate", true);

companion object {
fun from(id: String?): ActionId? {
return values().firstOrNull { it.id == id }
}
}
}
}
Loading

0 comments on commit bb963f9

Please sign in to comment.