diff --git a/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json b/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json index f1ccd490a..1aebf3510 100644 --- a/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json +++ b/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json @@ -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')" ] } } \ No newline at end of file diff --git a/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java b/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java index c3df1f8c0..ac734a35e 100644 --- a/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java +++ b/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java @@ -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, diff --git a/app/src/main/java/im/conversations/android/database/dao/GroupDao.java b/app/src/main/java/im/conversations/android/database/dao/GroupDao.java new file mode 100644 index 000000000..7831aa39f --- /dev/null +++ b/app/src/main/java/im/conversations/android/database/dao/GroupDao.java @@ -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(); +} diff --git a/app/src/main/java/im/conversations/android/database/dao/RosterDao.java b/app/src/main/java/im/conversations/android/database/dao/RosterDao.java index 7648afb0b..d04c851d7 100644 --- a/app/src/main/java/im/conversations/android/database/dao/RosterDao.java +++ b/app/src/main/java/im/conversations/android/database/dao/RosterDao.java @@ -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 groups) { + for (final String group : groups) { + if (Strings.isNullOrEmpty(group)) { + continue; + } + insertRosterGroup(RosterItemGroupEntity.of(rosterItemId, getOrCreateId(group))); + } } @Insert - protected abstract void insertRosterGroups(Collection entities); + protected abstract void insertRosterGroup(RosterItemGroupEntity entity); } diff --git a/app/src/main/java/im/conversations/android/database/entity/BookmarkGroupEntity.java b/app/src/main/java/im/conversations/android/database/entity/BookmarkGroupEntity.java new file mode 100644 index 000000000..6fdf27046 --- /dev/null +++ b/app/src/main/java/im/conversations/android/database/entity/BookmarkGroupEntity.java @@ -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; + } +} diff --git a/app/src/main/java/im/conversations/android/database/entity/GroupEntity.java b/app/src/main/java/im/conversations/android/database/entity/GroupEntity.java new file mode 100644 index 000000000..dd3f9950b --- /dev/null +++ b/app/src/main/java/im/conversations/android/database/entity/GroupEntity.java @@ -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; + } +} diff --git a/app/src/main/java/im/conversations/android/database/entity/RosterItemGroupEntity.java b/app/src/main/java/im/conversations/android/database/entity/RosterItemGroupEntity.java index b73c70b4a..b518ddc80 100644 --- a/app/src/main/java/im/conversations/android/database/entity/RosterItemGroupEntity.java +++ b/app/src/main/java/im/conversations/android/database/entity/RosterItemGroupEntity.java @@ -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; } } diff --git a/app/src/main/java/im/conversations/android/notification/ForegroundServiceNotification.java b/app/src/main/java/im/conversations/android/notification/ForegroundServiceNotification.java index ded7b2f90..de3d89332 100644 --- a/app/src/main/java/im/conversations/android/notification/ForegroundServiceNotification.java +++ b/app/src/main/java/im/conversations/android/notification/ForegroundServiceNotification.java @@ -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; } diff --git a/app/src/main/java/im/conversations/android/receiver/EventReceiver.java b/app/src/main/java/im/conversations/android/receiver/EventReceiver.java index c7b01b9a3..4b891e3ec 100644 --- a/app/src/main/java/im/conversations/android/receiver/EventReceiver.java +++ b/app/src/main/java/im/conversations/android/receiver/EventReceiver.java @@ -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; diff --git a/app/src/main/java/im/conversations/android/service/ForegroundService.java b/app/src/main/java/im/conversations/android/service/ForegroundService.java index b0eb8dcfa..1ee86972b 100644 --- a/app/src/main/java/im/conversations/android/service/ForegroundService.java +++ b/app/src/main/java/im/conversations/android/service/ForegroundService.java @@ -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); } diff --git a/app/src/main/java/im/conversations/android/ui/activity/MainActivity.java b/app/src/main/java/im/conversations/android/ui/activity/MainActivity.java index ab1cd4df6..7329d533d 100644 --- a/app/src/main/java/im/conversations/android/ui/activity/MainActivity.java +++ b/app/src/main/java/im/conversations/android/ui/activity/MainActivity.java @@ -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 {