Detailed changes
@@ -301,6 +301,15 @@ android {
unitTests {
includeAndroidResources = true
}
+ managedDevices {
+ localDevices {
+ pixel2api30 {
+ device = "Pixel 2"
+ apiLevel = 30
+ systemImageSource = "aosp-atd"
+ }
+ }
+ }
}
subprojects {
@@ -0,0 +1,197 @@
+package eu.siacs.conversations.persistance;
+
+import android.content.ContentValues;
+import android.database.sqlite.SQLiteDatabase;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.UUID;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.xmpp.model.disco.info.Feature;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+
+@RunWith(AndroidJUnit4.class)
+public class DatabaseBackendTest {
+ private record AccountFixture(String uuid, String username, String server) {
+ void write(DatabaseBackend db) {
+ final var cv = new ContentValues();
+ cv.put("uuid", uuid);
+ cv.put("username", username);
+ cv.put("server", server);
+ cv.put("password", "test");
+ cv.put("options", 0);
+ db.getWritableDatabase().insertWithOnConflict(
+ "accounts", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+ }
+
+ private record ConversationFixture(
+ String conversationUuid,
+ AccountFixture account,
+ String name,
+ String contactJid,
+ String attributes,
+ HashMap<MucOptions.User.OccupantId, MucOptions.User.CacheEntry> occupantCache
+ ) {
+ void writeConversation(DatabaseBackend db) {
+ final var cv = new ContentValues();
+ cv.put("uuid", conversationUuid);
+ cv.put("name", name);
+ cv.put("contactUuid", "");
+ cv.put("accountUuid", account.uuid());
+ cv.put("contactJid", contactJid);
+ cv.put("created", System.currentTimeMillis());
+ cv.put("status", Conversation.STATUS_AVAILABLE);
+ cv.put("mode", Conversation.MODE_MULTI);
+ cv.put("attributes", attributes);
+ db.getWritableDatabase().insert("conversations", null, cv);
+ }
+
+ void writeOccupants(DatabaseBackend db) {
+ for (final var entry : occupantCache.entrySet()) {
+ final var cv = new ContentValues();
+ cv.put(MucOptions.User.CacheEntry.OCCUPANT_ID, entry.getKey().inner());
+ cv.put(MucOptions.User.CacheEntry.CONVERSATION_UUID, conversationUuid);
+ cv.put(MucOptions.User.CacheEntry.AVATAR, entry.getValue().avatar());
+ cv.put(MucOptions.User.CacheEntry.NICK, entry.getValue().nick());
+ db.getWritableDatabase().insert(
+ MucOptions.User.CacheEntry.TABLENAME, null, cv);
+ }
+ }
+
+ void writeAll(DatabaseBackend db) {
+ writeConversation(db);
+ writeOccupants(db);
+ }
+
+ Conversation extractAndConfigure(DatabaseBackend db)
+ {
+ final var conversations = db.getConversations(Conversation.STATUS_AVAILABLE);
+ Assert.assertNotNull("getConversations should not return null", conversations);
+
+ Conversation match = null;
+ for (final var c : conversations) {
+ if (conversationUuid.equals(c.getUuid())) {
+ match = c;
+ break;
+ }
+ }
+ Assert.assertNotNull(
+ "Fixture conversation " + conversationUuid + " not found", match);
+
+ match.setAccount(db.getAccounts().get(0));
+ match.getMucOptions().updateConfiguration(INFO_QUERY_WITH_OCCUPANT_ID);
+ match.putAllInMucOccupantCache(db.getMucUsersForConversation(match));
+ return match;
+ }
+ }
+
+ private DatabaseBackend db;
+ private static final InfoQuery INFO_QUERY_WITH_OCCUPANT_ID = new InfoQuery();
+
+ private static AccountFixture ACCOUNT;
+ private static ConversationFixture CONFORMING;
+ private static ConversationFixture NO_CACHED_MUC_USERS;
+ private static ConversationFixture[] FIXTURES;
+
+ @BeforeClass
+ public static void setupClass() throws JSONException {
+ final var occupantIdFeature = new Feature();
+ occupantIdFeature.setVar(Namespace.OCCUPANT_ID);
+ INFO_QUERY_WITH_OCCUPANT_ID.addChild(occupantIdFeature);
+
+ ACCOUNT = new AccountFixture(
+ UUID.randomUUID().toString(), "test", "example.com");
+
+ final var conformingCache =
+ new HashMap<MucOptions.User.OccupantId, MucOptions.User.CacheEntry>();
+ conformingCache.put(
+ new MucOptions.User.OccupantId(UUID.randomUUID().toString()),
+ new MucOptions.User.CacheEntry(UUID.randomUUID().toString(), "ConformingUser"));
+
+ CONFORMING = new ConversationFixture(
+ UUID.randomUUID().toString(),
+ ACCOUNT,
+ "Normal MUC",
+ "normalroom@conference.example.com",
+ new JSONObject().put("mucNick", "testMucNick").toString(),
+ conformingCache
+ );
+
+ NO_CACHED_MUC_USERS = new ConversationFixture(
+ UUID.randomUUID().toString(),
+ ACCOUNT,
+ "Empty Cache MUC",
+ "emptycache@conference.example.com",
+ new JSONObject().put("mucNick", "testMucNick").toString(),
+ new HashMap<>()
+ );
+
+ FIXTURES = new ConversationFixture[] { CONFORMING, NO_CACHED_MUC_USERS };
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ db = DatabaseBackend.getInstance(
+ ApplicationProvider.getApplicationContext());
+ ACCOUNT.write(db);
+ for (final var fixture : FIXTURES) {
+ fixture.writeAll(db);
+ }
+ }
+
+ @After
+ public void tearDown() {
+ SQLiteDatabase sqDb = db.getWritableDatabase();
+ sqDb.delete(Conversation.TABLENAME, null, null);
+ sqDb.delete(Account.TABLENAME, null, null);
+ sqDb.delete(MucOptions.User.CacheEntry.TABLENAME, null, null);
+ }
+
+ @Test
+ public void getConversationsCorrectlyReadsMucUsers() throws Exception {
+ Assert.assertTrue(
+ "Occupant cache should be empty when no occupants are written",
+ NO_CACHED_MUC_USERS
+ .extractAndConfigure(db)
+ .getMucOccupantCache()
+ .isEmpty()
+ );
+
+ Assert.assertEquals(
+ "Cached entries should match fixture",
+ CONFORMING
+ .extractAndConfigure(db)
+ .getMucOccupantCache(),
+ CONFORMING.occupantCache()
+ );
+ }
+
+ @Test
+ public void updateConversationWritesMucOccupantsCache() throws Exception {
+ final var conversation = NO_CACHED_MUC_USERS.extractAndConfigure(db);
+ conversation.putAllInMucOccupantCache(CONFORMING.occupantCache());
+ db.updateConversation(conversation);
+
+ final var readBackCache = db.getMucUsersForConversation(conversation);
+ Assert.assertEquals(
+ "Cache should match after updateConversation",
+ CONFORMING.occupantCache(),
+ readBackCache
+ );
+ }
+}
@@ -48,6 +48,7 @@ import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.webkit.WebChromeClient;
import android.util.DisplayMetrics;
+import android.util.Log;
import android.util.LruCache;
import android.util.Pair;
import android.util.SparseArray;
@@ -77,6 +78,7 @@ import com.cheogram.android.WebxdcPage;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.textfield.TextInputLayout;
+import com.google.common.base.Functions;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
@@ -85,6 +87,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
+import im.conversations.android.xmpp.model.occupant.OccupantId;
import io.ipfs.cid.Cid;
import io.michaelrocks.libphonenumber.android.NumberParseException;
@@ -105,10 +108,13 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
+import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@@ -116,6 +122,7 @@ import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Function;
+import java.util.function.Predicate;
import me.saket.bettermovementmethod.BetterLinkMovementMethod;
@@ -161,21 +168,13 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.forms.Option;
import eu.siacs.conversations.xmpp.mam.MamReference;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.concurrent.atomic.AtomicBoolean;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import static eu.siacs.conversations.entities.Bookmark.printableValue;
import im.conversations.android.xmpp.model.stanza.Iq;
public class Conversation extends AbstractEntity
implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
+
+ public static final String TAG = "eu.siacs.conversations.entities.Conversation";
public static final String TABLENAME = "conversations";
public static final int STATUS_AVAILABLE = 0;
@@ -190,6 +189,24 @@ public class Conversation extends AbstractEntity
public static final String MODE = "mode";
public static final String ATTRIBUTES = "attributes";
+ public static final String[] ALL_COLUMNS = new String[] {
+ UUID,
+ NAME,
+ ACCOUNT,
+ CONTACT,
+ CONTACTJID,
+ STATUS,
+ CREATED,
+ MODE,
+ String.format(
+ "SUBSTR(%s, 0, %d) AS %s",
+ ATTRIBUTES,
+ Short.MAX_VALUE << 1,
+ ATTRIBUTES
+ )
+ };
+
+
public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
public static final String ATTRIBUTE_NOTIFY_REPLIES = "notify_replies";
@@ -220,6 +237,9 @@ public class Conversation extends AbstractEntity
private final JSONObject attributes;
private Jid nextCounterpart;
private final transient AtomicReference<MucOptions> mucOptions = new AtomicReference<>();
+ private transient ConcurrentMap<
+ MucOptions.User.OccupantId, MucOptions.User.CacheEntry
+ > mucOccupantCache = new ConcurrentHashMap<>();
private boolean messagesLeftOnServer = true;
private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
@@ -271,7 +291,7 @@ public class Conversation extends AbstractEntity
this.attributes = parseAttributes(attributes);
}
- private static JSONObject parseAttributes(final String attributes) {
+ private static JSONObject parseAttributes(@Nullable final String attributes) {
if (Strings.isNullOrEmpty(attributes)) {
return new JSONObject();
} else {
@@ -1162,6 +1182,70 @@ public class Conversation extends AbstractEntity
return this.nextCounterpart;
}
+ protected String getCachedOccupantNick(MucOptions.User.OccupantId cacheKey) {
+ final var cacheEntry = this.getMucOccupantCache().get(cacheKey);
+ if (cacheEntry == null) {
+ return null;
+ }
+ return cacheEntry.nick();
+ }
+
+ protected String getCachedOccupantAvatar(MucOptions.User.OccupantId cacheKey) {
+ final var cacheEntry = this.getMucOccupantCache().get(cacheKey);
+ if (cacheEntry == null) {
+ return null;
+ }
+ return cacheEntry.avatar();
+ }
+
+ protected boolean setCachedOccupantAvatar(MucOptions.User.OccupantId cacheKey, String newAvatar) {
+ final var newEntry = new MucOptions.User.CacheEntry(newAvatar, null);
+ return this.mucOccupantCache.merge(
+ cacheKey,
+ newEntry,
+ (prev, next) -> new MucOptions.User.CacheEntry(newAvatar, prev.nick())
+ ).equals(newEntry);
+ }
+
+ protected boolean setCachedOccupantNick(MucOptions.User.OccupantId cacheKey, String newNick) {
+ final var newEntry = new MucOptions.User.CacheEntry(null, newNick);
+ return this.mucOccupantCache.merge(
+ cacheKey,
+ newEntry,
+ (prev, next) -> new MucOptions.User.CacheEntry(prev.avatar(), newNick)
+ ).equals(newEntry);
+ }
+
+ public Map<MucOptions.User.OccupantId, MucOptions.User.CacheEntry> getMucOccupantCache() {
+ return this.mucOccupantCache.entrySet().stream().collect(
+ Collectors.toUnmodifiableMap(
+ Map.Entry::getKey,
+ Map.Entry::getValue
+ )
+ );
+ }
+
+ public void putAllInMucOccupantCache(Map<MucOptions.User.OccupantId, MucOptions.User.CacheEntry> newEntries) {
+ this.mucOccupantCache.putAll(newEntries);
+ }
+
+ public List<ContentValues> mucOccupantCacheAsContentValues() {
+ final var cvs = new ArrayList<ContentValues>();
+ mucOccupantCache.entrySet().forEach((entry) -> {
+ final var cv = new ContentValues();
+ final var occupantId = entry.getKey();
+ final var cacheEntry = entry.getValue();
+ final var avatar = cacheEntry.avatar();
+ final var nick = cacheEntry.nick();
+ cv.put(MucOptions.User.CacheEntry.OCCUPANT_ID, occupantId.inner());
+ cv.put(MucOptions.User.CacheEntry.CONVERSATION_UUID, this.getUuid());
+ cv.put(MucOptions.User.CacheEntry.AVATAR, avatar);
+ cv.put(MucOptions.User.CacheEntry.NICK, nick);
+ cvs.add(cv);
+ });
+ return cvs;
+ }
+
public void setNextCounterpart(Jid jid) {
this.nextCounterpart = jid;
}
@@ -1,5 +1,6 @@
package eu.siacs.conversations.entities;
+import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
@@ -14,6 +15,8 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
+import com.google.gson.JsonObject;
+
import de.gultsch.common.IntMap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.services.AvatarService;
@@ -27,25 +30,32 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.pep.Avatar;
import eu.siacs.conversations.xml.Element;
-
+import im.conversations.android.xmpp.model.Hash;
import im.conversations.android.xmpp.model.data.Data;
import im.conversations.android.xmpp.model.data.Field;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.muc.Affiliation;
import im.conversations.android.xmpp.model.muc.Item;
import im.conversations.android.xmpp.model.muc.Role;
+
+import java.io.Serializable;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
-public class MucOptions {
+import org.json.JSONException;
+import org.json.JSONObject;
+public class MucOptions {
private static final IntMap<Affiliation> AFFILIATION_RANKS =
new IntMap<>(
new ImmutableMap.Builder<Affiliation, Integer>()
@@ -121,6 +131,7 @@ public class MucOptions {
}
}
+
public void setAutoPushConfiguration(final boolean auto) {
this.mAutoPushConfiguration = auto;
}
@@ -338,6 +349,8 @@ public class MucOptions {
// returns true if real jid was new;
public boolean updateUser(User user) {
+ assert(user.options == null || user.options == this);
+
User old;
boolean realJidFound = false;
if (user.fullJid == null && user.realJid != null) {
@@ -902,6 +915,18 @@ public class MucOptions {
}
public static class User implements Comparable<User>, AvatarService.Avatarable {
+ public record CacheEntry(String avatar, String nick) {
+ public static final String TABLENAME = "muc_user";
+ public static final String AVATAR = "avatar";
+ public static final String NICK = "nick";
+ public static final String OCCUPANT_ID = "occupant_id";
+ public static final String CONVERSATION_UUID = "conversation_uuid";
+
+ public static CacheEntry fromUser(final User user) {
+ return new CacheEntry(user.getAvatar(), user.getNick());
+ }
+ }
+ public record OccupantId(String inner) { }
private Role role = Role.NONE;
private Affiliation affiliation = Affiliation.NONE;
private Jid realJid;
@@ -922,15 +947,16 @@ public class MucOptions {
this.nick = nick;
this.hats = hats;
+ final var cacheKey = new OccupantId(occupantId);
if (occupantId != null && options != null) {
- avatar = options.getConversation().getAttribute("occupantAvatar/" + occupantId);
+ avatar = options.conversation.getCachedOccupantAvatar(cacheKey);
if (nick == null) {
- this.nick = options.getConversation().getAttribute("occupantNick/" + occupantId);
+ this.nick = options.conversation.getCachedOccupantNick(cacheKey);
} else if (!getNick().equals(getName())) {
- options.getConversation().setAttribute("occupantNick/" + occupantId, nick);
+ options.conversation.setCachedOccupantNick(cacheKey, nick);
} else {
- options.getConversation().setAttribute("occupantNick/" + occupantId, (String) null);
+ options.conversation.setCachedOccupantNick(cacheKey, null);
}
}
}
@@ -1014,7 +1040,7 @@ public class MucOptions {
public boolean setAvatar(final String avatar) {
if (occupantId != null) {
- options.getConversation().setAttribute("occupantAvatar/" + occupantId, getContact() == null && avatar != null ? avatar : null);
+ options.conversation.setCachedOccupantAvatar(new OccupantId(occupantId), avatar);
}
if (this.avatar != null && this.avatar.equals(avatar)) {
return false;
@@ -4,7 +4,9 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteAbortException;
import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
import android.os.Build;
@@ -12,7 +14,6 @@ import android.os.Environment;
import android.os.SystemClock;
import android.util.Base64;
import android.util.Log;
-
import com.cheogram.android.WebxdcUpdate;
import com.google.common.base.Stopwatch;
@@ -21,6 +22,8 @@ import com.google.common.collect.HashMultimap;
import org.json.JSONException;
import org.json.JSONObject;
+import org.jxmpp.jid.parts.Localpart;
+import org.jxmpp.stringprep.XmppStringprepException;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
@@ -32,6 +35,7 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
@@ -47,6 +51,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
+import im.conversations.android.xmpp.model.occupant.OccupantId;
import io.ipfs.cid.Cid;
import com.google.common.collect.ImmutableMap;
@@ -68,38 +73,13 @@ import eu.siacs.conversations.utils.CursorUtils;
import eu.siacs.conversations.utils.FtsUtils;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.Resolver;
+import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.mam.MamReference;
import im.conversations.android.xml.XmlElementReader;
import im.conversations.android.xmpp.EntityCapabilities;
import im.conversations.android.xmpp.EntityCapabilities2;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArrayList;
-import org.json.JSONObject;
-import org.jxmpp.jid.parts.Localpart;
-import org.jxmpp.stringprep.XmppStringprepException;
-import org.whispersystems.libsignal.IdentityKey;
-import org.whispersystems.libsignal.IdentityKeyPair;
-import org.whispersystems.libsignal.InvalidKeyException;
-import org.whispersystems.libsignal.SignalProtocolAddress;
-import org.whispersystems.libsignal.state.PreKeyRecord;
-import org.whispersystems.libsignal.state.SessionRecord;
-import org.whispersystems.libsignal.state.SignedPreKeyRecord;
public class DatabaseBackend extends SQLiteOpenHelper {
@@ -541,6 +521,19 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("PRAGMA cheogram.user_version = 12");
}
+ if (cheogramVersion < 13) {
+ db.execSQL(
+ "CREATE TABLE cheogram." + MucOptions.User.CacheEntry.TABLENAME + " (" +
+ "conversation_uuid TEXT NOT NULL, " +
+ "occupant_id TEXT NOT NULL, " +
+ "nick TEXT, " +
+ "avatar TEXT, " +
+ "PRIMARY KEY (conversation_uuid, occupant_id)" +
+ ")"
+ );
+ db.execSQL("PRAGMA cheogram.user_version = 13");
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
@@ -549,7 +542,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
@Override
public void onConfigure(SQLiteDatabase db) {
- db.execSQL("PRAGMA foreign_keys=ON");
+ db.setForeignKeyConstraintsEnabled(true);
db.rawQuery("PRAGMA secure_delete=ON", null).close();
db.execSQL("ATTACH DATABASE ? AS cheogram", new Object[]{context.getDatabasePath("cheogram").getPath()});
cheogramMigrate(db);
@@ -1628,13 +1621,45 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return templates;
}
+ public HashMap<MucOptions.User.OccupantId, MucOptions.User.CacheEntry>
+ getMucUsersForConversation(Conversation conversation)
+ {
+ HashMap<MucOptions.User.OccupantId, MucOptions.User.CacheEntry> cache = new HashMap<>();
+ Cursor cursor = null;
+ try {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = {conversation.getUuid()};
+ cursor = db.rawQuery(
+ "select * from "
+ + MucOptions.User.CacheEntry.TABLENAME
+ + " where "
+ + MucOptions.User.CacheEntry.CONVERSATION_UUID
+ + "= ?",
+ selectionArgs
+ );
+ while (cursor.moveToNext()) {
+ final var avatar = cursor.getString(cursor.getColumnIndexOrThrow(MucOptions.User.CacheEntry.AVATAR));
+ final var nick = cursor.getString(cursor.getColumnIndexOrThrow(MucOptions.User.CacheEntry.NICK));
+ final var occupantId = cursor.getString(cursor.getColumnIndexOrThrow(MucOptions.User.CacheEntry.OCCUPANT_ID));
+ cache.put(
+ new MucOptions.User.OccupantId(occupantId),
+ new MucOptions.User.CacheEntry(avatar, nick)
+ );
+ }
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+
+ return cache;
+ }
+
public CopyOnWriteArrayList<Conversation> getConversations(int status) {
CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
String[] selectionArgs = {Integer.toString(status)};
Cursor cursor =
db.rawQuery(
- "select * from "
+ "select " + String.join(", ", Conversation.ALL_COLUMNS) + " from "
+ Conversation.TABLENAME
+ " where "
+ Conversation.STATUS
@@ -1649,6 +1674,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if (conversation.getJid() instanceof Jid.Invalid) {
continue;
}
+ conversation.putAllInMucOccupantCache(getMucUsersForConversation(conversation));
list.add(conversation);
}
cursor.close();
@@ -2148,14 +2174,27 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
- public void updateConversation(final Conversation conversation) {
+ public void updateConversation(final Conversation conversation) throws SQLiteException {
final SQLiteDatabase db = this.getWritableDatabase();
- final String[] args = {conversation.getUuid()};
- db.update(
- Conversation.TABLENAME,
- conversation.getContentValues(),
- Conversation.UUID + "=?",
- args);
+ db.beginTransaction();
+ try {
+ final String[] args = {conversation.getUuid()};
+ db.update(
+ Conversation.TABLENAME,
+ conversation.getContentValues(),
+ Conversation.UUID + "=?",
+ args);
+ for (final var cv : conversation.mucOccupantCacheAsContentValues()) {
+ db.insertWithOnConflict(
+ MucOptions.User.CacheEntry.TABLENAME,
+ null,
+ cv,
+ SQLiteDatabase.CONFLICT_REPLACE);
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
}
public List<Account> getAccounts() {
@@ -0,0 +1,91 @@
+package eu.siacs.conversations.entities;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.ConscryptMode;
+
+import android.os.Build;
+import eu.siacs.conversations.Conversations;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.disco.info.Feature;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import junit.framework.Assert;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = Build.VERSION_CODES.TIRAMISU, application = Conversations.class)
+@ConscryptMode(ConscryptMode.Mode.OFF)
+public class ConversationTest {
+ private static final InfoQuery INFO_QUERY_WITH_OCCUPANT_ID = new InfoQuery();
+ private static final InfoQuery INFO_QUERY_WITHOUT_OCCUPANT_ID = new InfoQuery();
+
+ private Conversation withOccupantId;
+ private Conversation withoutOccupantId;
+ private Conversation nullMucOptions;
+
+ @BeforeClass
+ public static void setupClass() {
+ final var occupantIdFeature = new Feature();
+ occupantIdFeature.setVar(Namespace.OCCUPANT_ID);
+ INFO_QUERY_WITH_OCCUPANT_ID.addChild(occupantIdFeature);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ final var account = mock(Account.class);
+ when(account.getJid()).thenReturn(Jid.ofLocalAndDomain("testAccount", "example.org"));
+
+ withOccupantId = new Conversation(
+ "Test MUC",
+ account,
+ Jid.ofLocalAndDomain("testMuc", "example.org"),
+ Conversation.MODE_MULTI
+ );
+ withOccupantId.getMucOptions().updateConfiguration(INFO_QUERY_WITH_OCCUPANT_ID);
+
+ withoutOccupantId = new Conversation(
+ "Test MUC",
+ account,
+ Jid.ofLocalAndDomain("testMuc", "example.org"),
+ Conversation.MODE_MULTI
+ );
+ withoutOccupantId.getMucOptions().updateConfiguration(INFO_QUERY_WITHOUT_OCCUPANT_ID);
+
+ nullMucOptions = new Conversation(
+ "Test MUC",
+ account,
+ Jid.ofLocalAndDomain("testMuc", "example.org"),
+ Conversation.MODE_MULTI
+ );
+ final var mucOptionsField = Conversation.class.getDeclaredField("mucOptions");
+ mucOptionsField.setAccessible(true);
+ ((AtomicReference<?>) mucOptionsField.get(nullMucOptions)).set(null);
+ }
+
+ @Test
+ public void getMucOccupantsCacheReturnsCacheWhenMucOptionsIsNull() throws Exception {
+ var cache = nullMucOptions.getMucOccupantCache();
+ Assert.assertNotNull("Should return cache when mucOptions is null", cache);
+ }
+
+ @Test
+ public void getMucOccupantsCacheReturnsCacheWhenFeatureSupported() throws Exception {
+ var cache = withOccupantId.getMucOccupantCache();
+ Assert.assertNotNull("Should return cache when occupant-id is supported", cache);
+ }
+
+ @Test
+ public void getMucOccupantsCacheReturnsCacheWhenFeatureNotSupported() throws Exception {
+ var cache = withoutOccupantId.getMucOccupantCache();
+ Assert.assertNotNull("Should return cache even when occupant-id is not supported", cache);
+ }
+}