Signal-Android/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt

396 wiersze
16 KiB
Kotlin

@file:Suppress("ktlint:filename")
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteConstraintException
import org.signal.core.util.SqlUtil
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
import org.thoughtcrime.securesms.recipients.RecipientId
import java.time.DayOfWeek
/**
* Database for maintaining Notification Profiles, Notification Profile Schedules, and Notification Profile allowed memebers.
*/
class NotificationProfileDatabase(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper), RecipientIdDatabaseReference {
companion object {
@JvmField
val CREATE_TABLE: Array<String> = arrayOf(NotificationProfileTable.CREATE_TABLE, NotificationProfileScheduleTable.CREATE_TABLE, NotificationProfileAllowedMembersTable.CREATE_TABLE)
@JvmField
val CREATE_INDEXES: Array<String> = arrayOf(NotificationProfileScheduleTable.CREATE_INDEX, NotificationProfileAllowedMembersTable.CREATE_INDEX)
}
private object NotificationProfileTable {
const val TABLE_NAME = "notification_profile"
const val ID = "_id"
const val NAME = "name"
const val EMOJI = "emoji"
const val COLOR = "color"
const val CREATED_AT = "created_at"
const val ALLOW_ALL_CALLS = "allow_all_calls"
const val ALLOW_ALL_MENTIONS = "allow_all_mentions"
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
$NAME TEXT NOT NULL UNIQUE,
$EMOJI TEXT NOT NULL,
$COLOR TEXT NOT NULL,
$CREATED_AT INTEGER NOT NULL,
$ALLOW_ALL_CALLS INTEGER NOT NULL DEFAULT 0,
$ALLOW_ALL_MENTIONS INTEGER NOT NULL DEFAULT 0
)
""".trimIndent()
}
private object NotificationProfileScheduleTable {
const val TABLE_NAME = "notification_profile_schedule"
const val ID = "_id"
const val NOTIFICATION_PROFILE_ID = "notification_profile_id"
const val ENABLED = "enabled"
const val START = "start"
const val END = "end"
const val DAYS_ENABLED = "days_enabled"
val DEFAULT_DAYS = listOf(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY).serialize()
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
$NOTIFICATION_PROFILE_ID INTEGER NOT NULL REFERENCES ${NotificationProfileTable.TABLE_NAME} (${NotificationProfileTable.ID}) ON DELETE CASCADE,
$ENABLED INTEGER NOT NULL DEFAULT 0,
$START INTEGER NOT NULL,
$END INTEGER NOT NULL,
$DAYS_ENABLED TEXT NOT NULL
)
""".trimIndent()
const val CREATE_INDEX = "CREATE INDEX notification_profile_schedule_profile_index ON $TABLE_NAME ($NOTIFICATION_PROFILE_ID)"
}
private object NotificationProfileAllowedMembersTable {
const val TABLE_NAME = "notification_profile_allowed_members"
const val ID = "_id"
const val NOTIFICATION_PROFILE_ID = "notification_profile_id"
const val RECIPIENT_ID = "recipient_id"
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
$NOTIFICATION_PROFILE_ID INTEGER NOT NULL REFERENCES ${NotificationProfileTable.TABLE_NAME} (${NotificationProfileTable.ID}) ON DELETE CASCADE,
$RECIPIENT_ID INTEGER NOT NULL,
UNIQUE($NOTIFICATION_PROFILE_ID, $RECIPIENT_ID) ON CONFLICT REPLACE
)
""".trimIndent()
const val CREATE_INDEX = "CREATE INDEX notification_profile_allowed_members_profile_index ON $TABLE_NAME ($NOTIFICATION_PROFILE_ID)"
}
fun createProfile(name: String, emoji: String, color: AvatarColor, createdAt: Long): NotificationProfileChangeResult {
val db = writableDatabase
db.beginTransaction()
try {
val profileValues = ContentValues().apply {
put(NotificationProfileTable.NAME, name)
put(NotificationProfileTable.EMOJI, emoji)
put(NotificationProfileTable.COLOR, color.serialize())
put(NotificationProfileTable.CREATED_AT, createdAt)
}
val profileId = db.insert(NotificationProfileTable.TABLE_NAME, null, profileValues)
if (profileId < 0) {
return NotificationProfileChangeResult.DuplicateName
}
val scheduleValues = ContentValues().apply {
put(NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID, profileId)
put(NotificationProfileScheduleTable.START, 900)
put(NotificationProfileScheduleTable.END, 1700)
put(NotificationProfileScheduleTable.DAYS_ENABLED, NotificationProfileScheduleTable.DEFAULT_DAYS)
}
db.insert(NotificationProfileScheduleTable.TABLE_NAME, null, scheduleValues)
db.setTransactionSuccessful()
return NotificationProfileChangeResult.Success(
NotificationProfile(
id = profileId,
name = name,
emoji = emoji,
createdAt = createdAt,
schedule = getProfileSchedule(profileId)
)
)
} finally {
db.endTransaction()
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
}
}
fun updateProfile(profileId: Long, name: String, emoji: String): NotificationProfileChangeResult {
val profileValues = ContentValues().apply {
put(NotificationProfileTable.NAME, name)
put(NotificationProfileTable.EMOJI, emoji)
}
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(profileId), profileValues)
return try {
val count = writableDatabase.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
if (count > 0) {
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
}
NotificationProfileChangeResult.Success(getProfile(profileId)!!)
} catch (e: SQLiteConstraintException) {
NotificationProfileChangeResult.DuplicateName
}
}
fun updateProfile(profile: NotificationProfile): NotificationProfileChangeResult {
val db = writableDatabase
db.beginTransaction()
try {
val profileValues = ContentValues().apply {
put(NotificationProfileTable.NAME, profile.name)
put(NotificationProfileTable.EMOJI, profile.emoji)
put(NotificationProfileTable.ALLOW_ALL_CALLS, profile.allowAllCalls.toInt())
put(NotificationProfileTable.ALLOW_ALL_MENTIONS, profile.allowAllMentions.toInt())
}
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(profile.id), profileValues)
try {
db.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
} catch (e: SQLiteConstraintException) {
return NotificationProfileChangeResult.DuplicateName
}
updateSchedule(profile.schedule, true)
db.delete(NotificationProfileAllowedMembersTable.TABLE_NAME, "${NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID} = ?", SqlUtil.buildArgs(profile.id))
profile.allowedMembers.forEach { recipientId ->
val allowedMembersValues = ContentValues().apply {
put(NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID, profile.id)
put(NotificationProfileAllowedMembersTable.RECIPIENT_ID, recipientId.serialize())
}
db.insert(NotificationProfileAllowedMembersTable.TABLE_NAME, null, allowedMembersValues)
}
db.setTransactionSuccessful()
return NotificationProfileChangeResult.Success(getProfile(profile.id)!!)
} finally {
db.endTransaction()
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
}
}
fun updateSchedule(schedule: NotificationProfileSchedule, silent: Boolean = false) {
val scheduleValues = ContentValues().apply {
put(NotificationProfileScheduleTable.ENABLED, schedule.enabled.toInt())
put(NotificationProfileScheduleTable.START, schedule.start)
put(NotificationProfileScheduleTable.END, schedule.end)
put(NotificationProfileScheduleTable.DAYS_ENABLED, schedule.daysEnabled.serialize())
}
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(schedule.id), scheduleValues)
writableDatabase.update(NotificationProfileScheduleTable.TABLE_NAME, scheduleValues, updateQuery.where, updateQuery.whereArgs)
if (!silent) {
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
}
}
fun setAllowedRecipients(profileId: Long, recipients: Set<RecipientId>): NotificationProfile {
val db = writableDatabase
db.beginTransaction()
try {
db.delete(NotificationProfileAllowedMembersTable.TABLE_NAME, "${NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID} = ?", SqlUtil.buildArgs(profileId))
recipients.forEach { recipientId ->
val allowedMembersValues = ContentValues().apply {
put(NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID, profileId)
put(NotificationProfileAllowedMembersTable.RECIPIENT_ID, recipientId.serialize())
}
db.insert(NotificationProfileAllowedMembersTable.TABLE_NAME, null, allowedMembersValues)
}
db.setTransactionSuccessful()
return getProfile(profileId)!!
} finally {
db.endTransaction()
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
}
}
fun addAllowedRecipient(profileId: Long, recipientId: RecipientId): NotificationProfile {
val allowedValues = ContentValues().apply {
put(NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID, profileId)
put(NotificationProfileAllowedMembersTable.RECIPIENT_ID, recipientId.serialize())
}
writableDatabase.insert(NotificationProfileAllowedMembersTable.TABLE_NAME, null, allowedValues)
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
return getProfile(profileId)!!
}
fun removeAllowedRecipient(profileId: Long, recipientId: RecipientId): NotificationProfile {
writableDatabase.delete(
NotificationProfileAllowedMembersTable.TABLE_NAME,
"${NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID} = ? AND ${NotificationProfileAllowedMembersTable.RECIPIENT_ID} = ?",
SqlUtil.buildArgs(profileId, recipientId)
)
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
return getProfile(profileId)!!
}
fun getProfiles(): List<NotificationProfile> {
val profiles: MutableList<NotificationProfile> = mutableListOf()
readableDatabase.query(NotificationProfileTable.TABLE_NAME, null, null, null, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
profiles += getProfile(cursor)
}
}
return profiles
}
fun getProfile(profileId: Long): NotificationProfile? {
return readableDatabase.query(NotificationProfileTable.TABLE_NAME, null, ID_WHERE, SqlUtil.buildArgs(profileId), null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
getProfile(cursor)
} else {
null
}
}
}
fun deleteProfile(profileId: Long) {
writableDatabase.delete(NotificationProfileTable.TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(profileId))
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
}
override fun remapRecipient(oldId: RecipientId, newId: RecipientId) {
val query = "${NotificationProfileAllowedMembersTable.RECIPIENT_ID} = ?"
val args = SqlUtil.buildArgs(oldId)
val values = ContentValues().apply {
put(NotificationProfileAllowedMembersTable.RECIPIENT_ID, newId.serialize())
}
databaseHelper.signalWritableDatabase.update(NotificationProfileAllowedMembersTable.TABLE_NAME, values, query, args)
ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers()
}
private fun getProfile(cursor: Cursor): NotificationProfile {
val profileId: Long = cursor.requireLong(NotificationProfileTable.ID)
return NotificationProfile(
id = profileId,
name = cursor.requireString(NotificationProfileTable.NAME)!!,
emoji = cursor.requireString(NotificationProfileTable.EMOJI)!!,
color = AvatarColor.deserialize(cursor.requireString(NotificationProfileTable.COLOR)),
createdAt = cursor.requireLong(NotificationProfileTable.CREATED_AT),
allowAllCalls = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_CALLS),
allowAllMentions = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_MENTIONS),
schedule = getProfileSchedule(profileId),
allowedMembers = getProfileAllowedMembers(profileId)
)
}
private fun getProfileSchedule(profileId: Long): NotificationProfileSchedule {
val query = SqlUtil.buildQuery("${NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID} = ?", profileId)
return readableDatabase.query(NotificationProfileScheduleTable.TABLE_NAME, null, query.where, query.whereArgs, null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
val daysEnabledString = cursor.requireString(NotificationProfileScheduleTable.DAYS_ENABLED) ?: ""
val daysEnabled: Set<DayOfWeek> = daysEnabledString.split(",")
.filter { it.isNotBlank() }
.map { it.toDayOfWeek() }
.toSet()
NotificationProfileSchedule(
id = cursor.requireLong(NotificationProfileScheduleTable.ID),
enabled = cursor.requireBoolean(NotificationProfileScheduleTable.ENABLED),
start = cursor.requireInt(NotificationProfileScheduleTable.START),
end = cursor.requireInt(NotificationProfileScheduleTable.END),
daysEnabled = daysEnabled
)
} else {
throw AssertionError("No schedule for $profileId")
}
}
}
private fun getProfileAllowedMembers(profileId: Long): Set<RecipientId> {
val allowed = mutableSetOf<RecipientId>()
val query = SqlUtil.buildQuery("${NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID} = ?", profileId)
readableDatabase.query(NotificationProfileAllowedMembersTable.TABLE_NAME, null, query.where, query.whereArgs, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
allowed += RecipientId.from(cursor.requireLong(NotificationProfileAllowedMembersTable.RECIPIENT_ID))
}
}
return allowed
}
sealed class NotificationProfileChangeResult {
data class Success(val notificationProfile: NotificationProfile) : NotificationProfileChangeResult()
object DuplicateName : NotificationProfileChangeResult()
}
}
private fun Iterable<DayOfWeek>.serialize(): String {
return joinToString(separator = ",", transform = { it.serialize() })
}
private fun String.toDayOfWeek(): DayOfWeek {
return when (this) {
"1" -> DayOfWeek.MONDAY
"2" -> DayOfWeek.TUESDAY
"3" -> DayOfWeek.WEDNESDAY
"4" -> DayOfWeek.THURSDAY
"5" -> DayOfWeek.FRIDAY
"6" -> DayOfWeek.SATURDAY
"7" -> DayOfWeek.SUNDAY
else -> throw AssertionError("Value ($this) does not map to a day")
}
}
private fun DayOfWeek.serialize(): String {
return when (this) {
DayOfWeek.MONDAY -> "1"
DayOfWeek.TUESDAY -> "2"
DayOfWeek.WEDNESDAY -> "3"
DayOfWeek.THURSDAY -> "4"
DayOfWeek.FRIDAY -> "5"
DayOfWeek.SATURDAY -> "6"
DayOfWeek.SUNDAY -> "7"
}
}