Allow blocking specific avatars (locally)

Stephen Paul Weber created

Change summary

src/main/java/eu/siacs/conversations/entities/MucOptions.java                 | 10 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java         | 27 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java             |  2 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java      |  9 
src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java | 14 
src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java                     | 15 
src/main/res/menu/muc_details_context.xml                                     |  4 
7 files changed, 79 insertions(+), 2 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/MucOptions.java 🔗

@@ -12,6 +12,8 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Set;
 
+import io.ipfs.cid.Cid;
+
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.services.AvatarService;
@@ -817,6 +819,14 @@ public class MucOptions {
             return avatar == null ? null : avatar.getFilename();
         }
 
+        public Cid getAvatarCid() {
+            if (avatar != null) {
+                return avatar.cid();
+            }
+            Avatar avatar = realJid != null ? getAccount().getRoster().getContact(realJid).getAvatar() : null;
+            return avatar == null ? null : avatar.cid();
+        }
+
         public Account getAccount() {
             return options.getAccount();
         }

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -274,6 +274,15 @@ public class DatabaseBackend extends SQLiteOpenHelper {
                 db.execSQL("PRAGMA cheogram.user_version = 5");
             }
 
+            if(cheogramVersion < 6) {
+                db.execSQL(
+                    "CREATE TABLE cheogram.blocked_media (" +
+                    "cid TEXT NOT NULL PRIMARY KEY" +
+                    ")"
+                );
+                db.execSQL("PRAGMA cheogram.user_version = 6");
+            }
+
             db.setTransactionSuccessful();
         } finally {
             db.endTransaction();
@@ -774,6 +783,24 @@ public class DatabaseBackend extends SQLiteOpenHelper {
         db.insertWithOnConflict("cheogram.cids", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
     }
 
+    public void blockMedia(Cid cid) {
+        SQLiteDatabase db = this.getWritableDatabase();
+        ContentValues cv = new ContentValues();
+        cv.put("cid", cid.toString());
+        db.insertWithOnConflict("cheogram.blocked_media", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
+    }
+
+    public boolean isBlockedMedia(Cid cid) {
+        SQLiteDatabase db = this.getReadableDatabase();
+        Cursor cursor = db.query("cheogram.blocked_media", new String[]{"count(*)"}, "cid=?", new String[]{cid.toString()}, null, null, null);
+        boolean is = false;
+        if (cursor.moveToNext()) {
+            is = cursor.getInt(0) > 0;
+        }
+        cursor.close();
+        return is;
+    }
+
     public void createConversation(Conversation conversation) {
         SQLiteDatabase db = this.getWritableDatabase();
         db.insert(Conversation.TABLENAME, null, conversation.getContentValues());

src/main/java/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -1600,7 +1600,7 @@ public class FileBackend {
         return new File(mXmppConnectionService.getFilesDir(), "/avatars/");
     }
 
-    private File getAvatarFile(String avatar) {
+    public File getAvatarFile(String avatar) {
         return new File(mXmppConnectionService.getCacheDir(), "/avatars/" + avatar);
     }
 

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -563,6 +563,10 @@ public class XmppConnectionService extends Service {
         this.databaseBackend.saveCid(cid, file);
     }
 
+    public void blockMedia(Cid cid) {
+        this.databaseBackend.blockMedia(cid);
+    }
+
     public AvatarService getAvatarService() {
         return this.mAvatarService;
     }
@@ -3889,6 +3893,11 @@ public class XmppConnectionService extends Service {
     }
 
     public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
+        if (databaseBackend.isBlockedMedia(avatar.cid())) {
+            if (callback != null) callback.error(0, null);
+            return;
+        }
+
         final String KEY = generateFetchKey(account, avatar);
         synchronized (this.mInProgressAvatarFetches) {
             if (mInProgressAvatarFetches.add(KEY)) {

src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java 🔗

@@ -57,6 +57,7 @@ public final class MucDetailsContextMenuHelper {
         MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message);
         if (user != null && user.getRealJid() != null) {
             MenuItem showContactDetails = menu.findItem(R.id.action_contact_details);
+            MenuItem blockAvatar = menu.findItem(R.id.action_block_avatar);
             MenuItem startConversation = menu.findItem(R.id.start_conversation);
             MenuItem giveMembership = menu.findItem(R.id.give_membership);
             MenuItem removeMembership = menu.findItem(R.id.remove_membership);
@@ -76,6 +77,9 @@ public final class MucDetailsContextMenuHelper {
             if ((contact != null && contact.showInRoster()) || mucOptions.isPrivateAndNonAnonymous()) {
                 showContactDetails.setVisible(contact == null || !contact.isSelf());
             }
+            if (user.getAvatar() != null) {
+                blockAvatar.setVisible(true);
+            }
             if ((activity instanceof ConferenceDetailsActivity || activity instanceof MucUsersActivity) && user.getRole() == MucOptions.Role.NONE) {
                 invite.setVisible(true);
             }
@@ -144,6 +148,14 @@ public final class MucDetailsContextMenuHelper {
                     activity.switchToContactDetails(contact, fingerprint);
                 }
                 return true;
+            case R.id.action_block_avatar:
+                activity.xmppConnectionService.getFileBackend().getAvatarFile(user.getAvatar()).delete();
+                activity.xmppConnectionService.blockMedia(user.getAvatarCid());
+                activity.avatarService().clear(user);
+                if (user.getContact() != null) activity.avatarService().clear(user.getContact());
+                user.setAvatar(null);
+                activity.xmppConnectionService.updateConversationUi();
+                return true;
             case R.id.start_conversation:
                 startConversation(user, activity);
                 return true;
@@ -226,4 +238,4 @@ public final class MucDetailsContextMenuHelper {
             activity.switchToConversation(newConversation);
         }
     }
-}
+}

src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java 🔗

@@ -2,6 +2,11 @@ package eu.siacs.conversations.xmpp.pep;
 
 import android.util.Base64;
 
+import java.security.NoSuchAlgorithmException;
+
+import io.ipfs.cid.Cid;
+
+import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.Jid;
 
@@ -82,6 +87,16 @@ public class Avatar {
 		}
 	}
 
+	public Cid cid() {
+		if (sha1sum == null) return null;
+
+		try {
+			return CryptoHelper.cid(CryptoHelper.hexToBytes(sha1sum), "sha-1");
+		} catch (final NoSuchAlgorithmException e) {
+			return null;
+		}
+	}
+
 	public static Avatar parsePresence(Element x) {
 		String hash = x == null ? null : x.findChildContent("photo");
 		if (hash == null) {

src/main/res/menu/muc_details_context.xml 🔗

@@ -8,6 +8,10 @@
         android:id="@+id/action_contact_details"
         android:title="@string/action_contact_details"
         android:visible="false" />
+    <item
+        android:id="@+id/action_block_avatar"
+        android:title="Block Avatar"
+        android:visible="false" />
     <item
         android:id="@+id/invite"
         android:title="@string/invite_again"