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,
"database": {
"version": 1,
"identityHash": "1952101c2c0d439fcd6c9d417f126a54",
"identityHash": "070e419bfe6857a47cda745017f04a57",
"entities": [
{
"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",
"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",
"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",
"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": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "rosterItemId",
"columnName": "rosterItemId",
@ -2219,27 +2299,28 @@
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
"fieldPath": "groupId",
"columnName": "groupId",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"autoGenerate": false,
"columnNames": [
"id"
"rosterItemId",
"groupId"
]
},
"indices": [
{
"name": "index_roster_group_rosterItemId",
"name": "index_roster_group_groupId",
"unique": false,
"columnNames": [
"rosterItemId"
"groupId"
],
"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": [
@ -2253,6 +2334,17 @@
"referencedColumns": [
"id"
]
},
{
"table": "group",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"groupId"
],
"referencedColumns": [
"id"
]
}
]
}
@ -2260,7 +2352,7 @@
"views": [],
"setupQueries": [
"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.BlockedItemEntity;
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.DiscoEntity;
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.DiscoIdentityEntity;
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.MessageEntity;
import im.conversations.android.database.entity.MessageReactionEntity;
@ -60,6 +62,7 @@ import im.conversations.android.database.entity.RosterItemGroupEntity;
AxolotlSignedPreKeyEntity.class,
BlockedItemEntity.class,
BookmarkEntity.class,
BookmarkGroupEntity.class,
ChatEntity.class,
DiscoEntity.class,
DiscoExtensionEntity.class,
@ -68,6 +71,7 @@ import im.conversations.android.database.entity.RosterItemGroupEntity;
DiscoFeatureEntity.class,
DiscoIdentityEntity.class,
DiscoItemEntity.class,
GroupEntity.class,
MessageEntity.class,
MessageStateEntity.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.Query;
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.RosterItemGroupEntity;
import im.conversations.android.database.model.Account;
@ -15,7 +15,7 @@ import java.util.Collection;
import org.jxmpp.jid.Jid;
@Dao
public abstract class RosterDao {
public abstract class RosterDao extends GroupDao {
@Insert(onConflict = REPLACE)
protected abstract long insert(RosterItemEntity rosterItem);
@ -35,11 +35,10 @@ public abstract class RosterDao {
clear(account.id);
for (final Item item : rosterItems) {
final long id = insert(RosterItemEntity.of(account.id, item));
insertRosterGroups(
Collections2.transform(
item.getGroups(), name -> RosterItemGroupEntity.of(id, name)));
insertRosterGroups(id, item.getGroups());
}
setRosterVersion(account.id, version);
deleteEmptyGroups();
}
public void update(
@ -54,13 +53,21 @@ public abstract class RosterDao {
}
final RosterItemEntity entity = RosterItemEntity.of(account.id, item);
final long id = insert(entity);
insertRosterGroups(
Collections2.transform(
item.getGroups(), name -> RosterItemGroupEntity.of(id, name)));
insertRosterGroups(id, item.getGroups());
}
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
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.ForeignKey;
import androidx.room.Index;
import androidx.room.PrimaryKey;
@Entity(
tableName = "roster_group",
foreignKeys =
@ForeignKey(
entity = RosterItemEntity.class,
parentColumns = {"id"},
childColumns = {"rosterItemId"},
onDelete = ForeignKey.CASCADE),
indices = {@Index(value = "rosterItemId")})
primaryKeys = {"rosterItemId", "groupId"},
foreignKeys = {
@ForeignKey(
entity = RosterItemEntity.class,
parentColumns = {"id"},
childColumns = {"rosterItemId"},
onDelete = ForeignKey.CASCADE),
@ForeignKey(
entity = GroupEntity.class,
parentColumns = {"id"},
childColumns = {"groupId"},
onDelete = ForeignKey.RESTRICT),
},
indices = {@Index(value = "groupId")})
public class RosterItemGroupEntity {
@PrimaryKey(autoGenerate = true)
public Long id;
@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();
entity.rosterItemId = rosterItemId;
entity.name = name;
entity.groupId = groupId;
return entity;
}
}

View file

@ -6,9 +6,7 @@ import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import androidx.core.content.ContextCompat;
import im.conversations.android.R;
import im.conversations.android.ui.activity.MainActivity;
import im.conversations.android.xmpp.ConnectionPool;
@ -53,7 +51,8 @@ public class ForegroundServiceNotification {
}
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) {
return;
}

View file

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

View file

@ -2,7 +2,6 @@ package im.conversations.android.service;
import android.content.Context;
import android.content.Intent;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleService;
import im.conversations.android.notification.ForegroundServiceNotification;
@ -41,7 +40,8 @@ public class ForegroundService extends LifecycleService {
public static void start(final Context context) {
try {
ContextCompat.startForegroundService(context, new Intent(context, ForegroundService.class));
ContextCompat.startForegroundService(
context, new Intent(context, ForegroundService.class));
} catch (final RuntimeException 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 androidx.appcompat.app.AppCompatActivity;
import im.conversations.android.service.ForegroundService;
public class MainActivity extends AppCompatActivity {