mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 18:00:02 +01:00
Add remote megaphone.
This commit is contained in:
committed by
Alex Hart
parent
820277800b
commit
bb963f9210
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user