store roster groups and bookmark groups in one table

This commit is contained in:
Daniel Gultsch 2023-02-19 09:28:50 +01:00
parent 2212c63810
commit c105c3420e
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
11 changed files with 233 additions and 48 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "1952101c2c0d439fcd6c9d417f126a54", "identityHash": "070e419bfe6857a47cda745017f04a57",
"entities": [ "entities": [
{ {
"tableName": "account", "tableName": "account",
@ -880,6 +880,66 @@
} }
] ]
}, },
{
"tableName": "bookmark_group",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookmarkId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`bookmarkId`, `groupId`), FOREIGN KEY(`bookmarkId`) REFERENCES `bookmark`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`groupId`) REFERENCES `group`(`id`) ON UPDATE NO ACTION ON DELETE RESTRICT )",
"fields": [
{
"fieldPath": "bookmarkId",
"columnName": "bookmarkId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupId",
"columnName": "groupId",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"bookmarkId",
"groupId"
]
},
"indices": [
{
"name": "index_bookmark_group_groupId",
"unique": false,
"columnNames": [
"groupId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_bookmark_group_groupId` ON `${TABLE_NAME}` (`groupId`)"
}
],
"foreignKeys": [
{
"table": "bookmark",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"bookmarkId"
],
"referencedColumns": [
"id"
]
},
{
"table": "group",
"onDelete": "RESTRICT",
"onUpdate": "NO ACTION",
"columns": [
"groupId"
],
"referencedColumns": [
"id"
]
}
]
},
{ {
"tableName": "chat", "tableName": "chat",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `type` TEXT, `archived` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `type` TEXT, `archived` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
@ -1412,6 +1472,32 @@
} }
] ]
}, },
{
"tableName": "group",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{ {
"tableName": "message", "tableName": "message",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `chatId` INTEGER NOT NULL, `receivedAt` INTEGER, `sentAt` INTEGER, `outgoing` INTEGER NOT NULL, `toBare` TEXT, `toResource` TEXT, `fromBare` TEXT, `fromResource` TEXT, `occupantId` TEXT, `messageId` TEXT, `stanzaId` TEXT, `stanzaIdVerified` INTEGER NOT NULL, `latestVersion` INTEGER, `acknowledged` INTEGER NOT NULL, `inReplyToMessageId` TEXT, `inReplyToStanzaId` TEXT, `inReplyToMessageEntityId` INTEGER, FOREIGN KEY(`chatId`) REFERENCES `chat`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`latestVersion`) REFERENCES `message_version`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`inReplyToMessageEntityId`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `chatId` INTEGER NOT NULL, `receivedAt` INTEGER, `sentAt` INTEGER, `outgoing` INTEGER NOT NULL, `toBare` TEXT, `toResource` TEXT, `fromBare` TEXT, `fromResource` TEXT, `occupantId` TEXT, `messageId` TEXT, `stanzaId` TEXT, `stanzaIdVerified` INTEGER NOT NULL, `latestVersion` INTEGER, `acknowledged` INTEGER NOT NULL, `inReplyToMessageId` TEXT, `inReplyToStanzaId` TEXT, `inReplyToMessageEntityId` INTEGER, FOREIGN KEY(`chatId`) REFERENCES `chat`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`latestVersion`) REFERENCES `message_version`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`inReplyToMessageEntityId`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
@ -2204,14 +2290,8 @@
}, },
{ {
"tableName": "roster_group", "tableName": "roster_group",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `rosterItemId` INTEGER NOT NULL, `name` TEXT, FOREIGN KEY(`rosterItemId`) REFERENCES `roster`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rosterItemId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`rosterItemId`, `groupId`), FOREIGN KEY(`rosterItemId`) REFERENCES `roster`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`groupId`) REFERENCES `group`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [ "fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{ {
"fieldPath": "rosterItemId", "fieldPath": "rosterItemId",
"columnName": "rosterItemId", "columnName": "rosterItemId",
@ -2219,27 +2299,28 @@
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "name", "fieldPath": "groupId",
"columnName": "name", "columnName": "groupId",
"affinity": "TEXT", "affinity": "INTEGER",
"notNull": false "notNull": true
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": true, "autoGenerate": false,
"columnNames": [ "columnNames": [
"id" "rosterItemId",
"groupId"
] ]
}, },
"indices": [ "indices": [
{ {
"name": "index_roster_group_rosterItemId", "name": "index_roster_group_groupId",
"unique": false, "unique": false,
"columnNames": [ "columnNames": [
"rosterItemId" "groupId"
], ],
"orders": [], "orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_roster_group_rosterItemId` ON `${TABLE_NAME}` (`rosterItemId`)" "createSql": "CREATE INDEX IF NOT EXISTS `index_roster_group_groupId` ON `${TABLE_NAME}` (`groupId`)"
} }
], ],
"foreignKeys": [ "foreignKeys": [
@ -2253,6 +2334,17 @@
"referencedColumns": [ "referencedColumns": [
"id" "id"
] ]
},
{
"table": "group",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"groupId"
],
"referencedColumns": [
"id"
]
} }
] ]
} }
@ -2260,7 +2352,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1952101c2c0d439fcd6c9d417f126a54')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '070e419bfe6857a47cda745017f04a57')"
] ]
} }
} }

View file

@ -28,6 +28,7 @@ import im.conversations.android.database.entity.AxolotlSessionEntity;
import im.conversations.android.database.entity.AxolotlSignedPreKeyEntity; import im.conversations.android.database.entity.AxolotlSignedPreKeyEntity;
import im.conversations.android.database.entity.BlockedItemEntity; import im.conversations.android.database.entity.BlockedItemEntity;
import im.conversations.android.database.entity.BookmarkEntity; import im.conversations.android.database.entity.BookmarkEntity;
import im.conversations.android.database.entity.BookmarkGroupEntity;
import im.conversations.android.database.entity.ChatEntity; import im.conversations.android.database.entity.ChatEntity;
import im.conversations.android.database.entity.DiscoEntity; import im.conversations.android.database.entity.DiscoEntity;
import im.conversations.android.database.entity.DiscoExtensionEntity; import im.conversations.android.database.entity.DiscoExtensionEntity;
@ -36,6 +37,7 @@ import im.conversations.android.database.entity.DiscoExtensionFieldValueEntity;
import im.conversations.android.database.entity.DiscoFeatureEntity; import im.conversations.android.database.entity.DiscoFeatureEntity;
import im.conversations.android.database.entity.DiscoIdentityEntity; import im.conversations.android.database.entity.DiscoIdentityEntity;
import im.conversations.android.database.entity.DiscoItemEntity; import im.conversations.android.database.entity.DiscoItemEntity;
import im.conversations.android.database.entity.GroupEntity;
import im.conversations.android.database.entity.MessageContentEntity; import im.conversations.android.database.entity.MessageContentEntity;
import im.conversations.android.database.entity.MessageEntity; import im.conversations.android.database.entity.MessageEntity;
import im.conversations.android.database.entity.MessageReactionEntity; import im.conversations.android.database.entity.MessageReactionEntity;
@ -60,6 +62,7 @@ import im.conversations.android.database.entity.RosterItemGroupEntity;
AxolotlSignedPreKeyEntity.class, AxolotlSignedPreKeyEntity.class,
BlockedItemEntity.class, BlockedItemEntity.class,
BookmarkEntity.class, BookmarkEntity.class,
BookmarkGroupEntity.class,
ChatEntity.class, ChatEntity.class,
DiscoEntity.class, DiscoEntity.class,
DiscoExtensionEntity.class, DiscoExtensionEntity.class,
@ -68,6 +71,7 @@ import im.conversations.android.database.entity.RosterItemGroupEntity;
DiscoFeatureEntity.class, DiscoFeatureEntity.class,
DiscoIdentityEntity.class, DiscoIdentityEntity.class,
DiscoItemEntity.class, DiscoItemEntity.class,
GroupEntity.class,
MessageEntity.class, MessageEntity.class,
MessageStateEntity.class, MessageStateEntity.class,
MessageContentEntity.class, MessageContentEntity.class,

View file

@ -0,0 +1,27 @@
package im.conversations.android.database.dao;
import androidx.room.Insert;
import androidx.room.Query;
import im.conversations.android.database.entity.GroupEntity;
public abstract class GroupDao {
public long getOrCreateId(final String name) {
final Long existing = getGroupId(name);
if (existing != null) {
return existing;
}
return insert(GroupEntity.of(name));
}
@Query("SELECT id FROM `group` WHERE name=:name")
abstract Long getGroupId(final String name);
@Insert
abstract Long insert(GroupEntity groupEntity);
@Query(
"DELETE from `group` WHERE id NOT IN(SELECT groupId FROM roster_group) AND id NOT"
+ " IN(SELECT groupId FROM bookmark_group)")
abstract void deleteEmptyGroups();
}

View file

@ -6,7 +6,7 @@ import androidx.room.Dao;
import androidx.room.Insert; import androidx.room.Insert;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Transaction; import androidx.room.Transaction;
import com.google.common.collect.Collections2; import com.google.common.base.Strings;
import im.conversations.android.database.entity.RosterItemEntity; import im.conversations.android.database.entity.RosterItemEntity;
import im.conversations.android.database.entity.RosterItemGroupEntity; import im.conversations.android.database.entity.RosterItemGroupEntity;
import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Account;
@ -15,7 +15,7 @@ import java.util.Collection;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@Dao @Dao
public abstract class RosterDao { public abstract class RosterDao extends GroupDao {
@Insert(onConflict = REPLACE) @Insert(onConflict = REPLACE)
protected abstract long insert(RosterItemEntity rosterItem); protected abstract long insert(RosterItemEntity rosterItem);
@ -35,11 +35,10 @@ public abstract class RosterDao {
clear(account.id); clear(account.id);
for (final Item item : rosterItems) { for (final Item item : rosterItems) {
final long id = insert(RosterItemEntity.of(account.id, item)); final long id = insert(RosterItemEntity.of(account.id, item));
insertRosterGroups( insertRosterGroups(id, item.getGroups());
Collections2.transform(
item.getGroups(), name -> RosterItemGroupEntity.of(id, name)));
} }
setRosterVersion(account.id, version); setRosterVersion(account.id, version);
deleteEmptyGroups();
} }
public void update( public void update(
@ -54,13 +53,21 @@ public abstract class RosterDao {
} }
final RosterItemEntity entity = RosterItemEntity.of(account.id, item); final RosterItemEntity entity = RosterItemEntity.of(account.id, item);
final long id = insert(entity); final long id = insert(entity);
insertRosterGroups( insertRosterGroups(id, item.getGroups());
Collections2.transform(
item.getGroups(), name -> RosterItemGroupEntity.of(id, name)));
} }
setRosterVersion(account.id, version); setRosterVersion(account.id, version);
deleteEmptyGroups();
}
protected void insertRosterGroups(final long rosterItemId, Collection<String> groups) {
for (final String group : groups) {
if (Strings.isNullOrEmpty(group)) {
continue;
}
insertRosterGroup(RosterItemGroupEntity.of(rosterItemId, getOrCreateId(group)));
}
} }
@Insert @Insert
protected abstract void insertRosterGroups(Collection<RosterItemGroupEntity> entities); protected abstract void insertRosterGroup(RosterItemGroupEntity entity);
} }

View file

@ -0,0 +1,36 @@
package im.conversations.android.database.entity;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
@Entity(
tableName = "bookmark_group",
primaryKeys = {"bookmarkId", "groupId"},
foreignKeys = {
@ForeignKey(
entity = BookmarkEntity.class,
parentColumns = {"id"},
childColumns = {"bookmarkId"},
onDelete = ForeignKey.CASCADE),
@ForeignKey(
entity = GroupEntity.class,
parentColumns = {"id"},
childColumns = {"groupId"},
onDelete = ForeignKey.RESTRICT),
},
indices = {@Index(value = "groupId")})
public class BookmarkGroupEntity {
@NonNull public Long bookmarkId;
@NonNull public Long groupId;
public static BookmarkGroupEntity of(long bookmarkId, final long groupId) {
final var entity = new BookmarkGroupEntity();
entity.bookmarkId = bookmarkId;
entity.groupId = groupId;
return entity;
}
}

View file

@ -0,0 +1,19 @@
package im.conversations.android.database.entity;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "group")
public class GroupEntity {
@PrimaryKey @NonNull public Long id;
@NonNull public String name;
public static GroupEntity of(final String name) {
final var entity = new GroupEntity();
entity.name = name;
return entity;
}
}

View file

@ -4,30 +4,33 @@ import androidx.annotation.NonNull;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.ForeignKey; import androidx.room.ForeignKey;
import androidx.room.Index; import androidx.room.Index;
import androidx.room.PrimaryKey;
@Entity( @Entity(
tableName = "roster_group", tableName = "roster_group",
foreignKeys = primaryKeys = {"rosterItemId", "groupId"},
@ForeignKey( foreignKeys = {
entity = RosterItemEntity.class, @ForeignKey(
parentColumns = {"id"}, entity = RosterItemEntity.class,
childColumns = {"rosterItemId"}, parentColumns = {"id"},
onDelete = ForeignKey.CASCADE), childColumns = {"rosterItemId"},
indices = {@Index(value = "rosterItemId")}) onDelete = ForeignKey.CASCADE),
@ForeignKey(
entity = GroupEntity.class,
parentColumns = {"id"},
childColumns = {"groupId"},
onDelete = ForeignKey.RESTRICT),
},
indices = {@Index(value = "groupId")})
public class RosterItemGroupEntity { public class RosterItemGroupEntity {
@PrimaryKey(autoGenerate = true)
public Long id;
@NonNull public Long rosterItemId; @NonNull public Long rosterItemId;
public String name; @NonNull public Long groupId;
public static RosterItemGroupEntity of(long rosterItemId, final String name) { public static RosterItemGroupEntity of(long rosterItemId, final long groupId) {
final var entity = new RosterItemGroupEntity(); final var entity = new RosterItemGroupEntity();
entity.rosterItemId = rosterItemId; entity.rosterItemId = rosterItemId;
entity.name = name; entity.groupId = groupId;
return entity; return entity;
} }
} }

View file

@ -6,9 +6,7 @@ import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import im.conversations.android.R; import im.conversations.android.R;
import im.conversations.android.ui.activity.MainActivity; import im.conversations.android.ui.activity.MainActivity;
import im.conversations.android.xmpp.ConnectionPool; import im.conversations.android.xmpp.ConnectionPool;
@ -53,7 +51,8 @@ public class ForegroundServiceNotification {
} }
public void update(final ConnectionPool.Summary summary) { public void update(final ConnectionPool.Summary summary) {
final var notificationManager = ContextCompat.getSystemService(service, NotificationManager.class); final var notificationManager =
ContextCompat.getSystemService(service, NotificationManager.class);
if (notificationManager == null) { if (notificationManager == null) {
return; return;
} }

View file

@ -3,7 +3,6 @@ package im.conversations.android.receiver;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import androidx.core.content.ContextCompat;
import im.conversations.android.service.ForegroundService; import im.conversations.android.service.ForegroundService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;

View file

@ -2,7 +2,6 @@ package im.conversations.android.service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleService; import androidx.lifecycle.LifecycleService;
import im.conversations.android.notification.ForegroundServiceNotification; import im.conversations.android.notification.ForegroundServiceNotification;
@ -41,7 +40,8 @@ public class ForegroundService extends LifecycleService {
public static void start(final Context context) { public static void start(final Context context) {
try { try {
ContextCompat.startForegroundService(context, new Intent(context, ForegroundService.class)); ContextCompat.startForegroundService(
context, new Intent(context, ForegroundService.class));
} catch (final RuntimeException e) { } catch (final RuntimeException e) {
LOGGER.error("Could not start foreground service", e); LOGGER.error("Could not start foreground service", e);
} }

View file

@ -2,7 +2,6 @@ package im.conversations.android.ui.activity;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import im.conversations.android.service.ForegroundService; import im.conversations.android.service.ForegroundService;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {