Add remote megaphone.

This commit is contained in:
Cody Henthorne
2022-05-11 14:33:54 -04:00
committed by Alex Hart
parent 820277800b
commit bb963f9210
20 changed files with 1069 additions and 322 deletions

View File

@@ -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)
)
}
}

View File

@@ -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()
@@ -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)
@@ -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
}
}

View File

@@ -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) {
@@ -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

View File

@@ -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 }
}
}
}
}