Add Notification profiles.

This commit is contained in:
Cody Henthorne
2021-12-08 13:22:36 -05:00
parent 31e0696395
commit 6c608e955e
106 changed files with 5692 additions and 238 deletions

View File

@@ -55,3 +55,5 @@ fun Cursor.optionalBlob(column: String): Optional<ByteArray> {
fun Cursor.isNull(column: String): Boolean {
return CursorUtil.isNull(this, column)
}
fun Boolean.toInt(): Int = if (this) 1 else 0

View File

@@ -4,6 +4,7 @@ import android.app.Application;
import androidx.annotation.NonNull;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
@@ -20,7 +21,7 @@ import java.util.concurrent.Executor;
*
* A replacement for the observer system in {@link Database}. We should move to this over time.
*/
public final class DatabaseObserver {
public class DatabaseObserver {
private final Application application;
private final Executor executor;
@@ -36,6 +37,7 @@ public final class DatabaseObserver {
private final Set<Observer> attachmentObservers;
private final Set<MessageObserver> messageUpdateObservers;
private final Map<Long, Set<MessageObserver>> messageInsertObservers;
private final Set<Observer> notificationProfileObservers;
public DatabaseObserver(Application application) {
this.application = application;
@@ -51,6 +53,7 @@ public final class DatabaseObserver {
this.attachmentObservers = new HashSet<>();
this.messageUpdateObservers = new HashSet<>();
this.messageInsertObservers = new HashMap<>();
this.notificationProfileObservers = new HashSet<>();
}
public void registerConversationListObserver(@NonNull Observer listener) {
@@ -119,6 +122,12 @@ public final class DatabaseObserver {
});
}
public void registerNotificationProfileObserver(@NotNull Observer listener) {
executor.execute(() -> {
notificationProfileObservers.add(listener);
});
}
public void unregisterObserver(@NonNull Observer listener) {
executor.execute(() -> {
conversationListObservers.remove(listener);
@@ -129,6 +138,7 @@ public final class DatabaseObserver {
stickerObservers.remove(listener);
stickerPackObservers.remove(listener);
attachmentObservers.remove(listener);
notificationProfileObservers.remove(listener);
});
}
@@ -231,6 +241,12 @@ public final class DatabaseObserver {
});
}
public void notifyNotificationProfileObservers() {
executor.execute(() -> {
notifySet(notificationProfileObservers);
});
}
private <K, V> void registerMapped(@NonNull Map<K, Set<V>> map, @NonNull K key, @NonNull V listener) {
Set<V> listeners = map.get(key);

View File

@@ -0,0 +1,388 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import net.zetetic.database.sqlcipher.SQLiteConstraintException
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 org.thoughtcrime.securesms.util.SqlUtil
import java.time.DayOfWeek
/**
* Database for maintaining Notification Profiles, Notification Profile Schedules, and Notification Profile allowed memebers.
*/
class NotificationProfileDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) {
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()
}
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"
}
}

View File

@@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.identities
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.notificationProfiles
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.sessions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
@@ -2592,6 +2593,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
// Reactions
reactions.remapRecipient(byE164, byAci)
// Notification Profiles
notificationProfiles.remapRecipient(byE164, byAci)
// Recipient
Log.w(TAG, "Deleting recipient $byE164", true)
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164))

View File

@@ -70,6 +70,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val avatarPickerDatabase: AvatarPickerDatabase = AvatarPickerDatabase(context, this)
val groupCallRingDatabase: GroupCallRingDatabase = GroupCallRingDatabase(context, this)
val reactionDatabase: ReactionDatabase = ReactionDatabase(context, this)
val notificationProfileDatabase: NotificationProfileDatabase = NotificationProfileDatabase(context, this)
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
db.enableWriteAheadLogging()
@@ -105,6 +106,8 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
executeStatements(db, SearchDatabase.CREATE_TABLE)
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE)
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE)
executeStatements(db, NotificationProfileDatabase.CREATE_TABLE)
executeStatements(db, RecipientDatabase.CREATE_INDEXS)
executeStatements(db, SmsDatabase.CREATE_INDEXS)
executeStatements(db, MmsDatabase.CREATE_INDEXS)
@@ -119,8 +122,11 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
executeStatements(db, PaymentDatabase.CREATE_INDEXES)
executeStatements(db, MessageSendLogDatabase.CREATE_INDEXES)
executeStatements(db, GroupCallRingDatabase.CREATE_INDEXES)
executeStatements(db, NotificationProfileDatabase.CREATE_INDEXES)
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS)
executeStatements(db, ReactionDatabase.CREATE_TRIGGERS)
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
val legacyHelper = ClassicOpenHelper(context)
val legacyDb = legacyHelper.writableDatabase
@@ -438,5 +444,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
@get:JvmName("unknownStorageIds")
val unknownStorageIds: UnknownStorageIdDatabase
get() = instance!!.storageIdDatabase
@get:JvmStatic
@get:JvmName("notificationProfiles")
val notificationProfiles: NotificationProfileDatabase
get() = instance!!.notificationProfileDatabase
}
}

View File

@@ -179,8 +179,9 @@ object SignalDatabaseMigrations {
private const val SENDER_KEY_SHARED_TIMESTAMP = 120
private const val REACTION_REFACTOR = 121
private const val PNI = 122
private const val NOTIFICATION_PROFILES = 123
const val DATABASE_VERSION = 122
const val DATABASE_VERSION = 123
@JvmStatic
fun migrate(context: Context, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@@ -2176,6 +2177,51 @@ object SignalDatabaseMigrations {
db.execSQL("ALTER TABLE recipient ADD COLUMN pni TEXT DEFAULT NULL")
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS recipient_pni_index ON recipient (pni)")
}
if (oldVersion < NOTIFICATION_PROFILES) {
db.execSQL(
// language=sql
"""
CREATE TABLE notification_profile (
_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()
)
db.execSQL(
// language=sql
"""
CREATE TABLE notification_profile_schedule (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,
enabled INTEGER NOT NULL DEFAULT 0,
start INTEGER NOT NULL,
end INTEGER NOT NULL,
days_enabled TEXT NOT NULL
)
""".trimIndent()
)
db.execSQL(
// language=sql
"""
CREATE TABLE notification_profile_allowed_members (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,
recipient_id INTEGER NOT NULL,
UNIQUE(notification_profile_id, recipient_id) ON CONFLICT REPLACE)
""".trimIndent()
)
db.execSQL("CREATE INDEX notification_profile_schedule_profile_index ON notification_profile_schedule (notification_profile_id)")
db.execSQL("CREATE INDEX notification_profile_allowed_members_profile_index ON notification_profile_allowed_members (notification_profile_id)")
}
}
@JvmStatic