add option to restrict avatar access model to contacts

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java   |  65 
src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java | 207 
src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java          |  18 
src/main/res/layout/activity_publish_profile_picture.xml                   |  36 
src/main/res/values/strings.xml                                            |   2 
5 files changed, 192 insertions(+), 136 deletions(-)

Detailed changes

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

@@ -4678,28 +4678,32 @@ public class XmppConnectionService extends Service {
                 .start();
     }
 
-    public void publishAvatar(
-            final Account account, final Uri image, final OnAvatarPublication callback) {
-        new Thread(
-                        () -> {
-                            final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
-                            final int size = Config.AVATAR_SIZE;
-                            final Avatar avatar =
-                                    getFileBackend().getPepAvatar(image, size, format);
-                            if (avatar != null) {
-                                if (!getFileBackend().save(avatar)) {
-                                    Log.d(Config.LOGTAG, "unable to save vcard");
-                                    callback.onAvatarPublicationFailed(
-                                            R.string.error_saving_avatar);
-                                    return;
-                                }
-                                publishAvatar(account, avatar, callback);
-                            } else {
-                                callback.onAvatarPublicationFailed(
-                                        R.string.error_publish_avatar_converting);
-                            }
-                        })
-                .start();
+    public void publishAvatarAsync(
+            final Account account,
+            final Uri image,
+            final boolean open,
+            final OnAvatarPublication callback) {
+        new Thread(() -> publishAvatar(account, image, open, callback)).start();
+    }
+
+    private void publishAvatar(
+            final Account account,
+            final Uri image,
+            final boolean open,
+            final OnAvatarPublication callback) {
+        final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
+        final int size = Config.AVATAR_SIZE;
+        final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
+        if (avatar != null) {
+            if (!getFileBackend().save(avatar)) {
+                Log.d(Config.LOGTAG, "unable to save vcard");
+                callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
+                return;
+            }
+            publishAvatar(account, avatar, open, callback);
+        } else {
+            callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
+        }
     }
 
     private void publishMucAvatar(
@@ -4753,10 +4757,13 @@ public class XmppConnectionService extends Service {
     }
 
     public void publishAvatar(
-            Account account, final Avatar avatar, final OnAvatarPublication callback) {
+            final Account account,
+            final Avatar avatar,
+            final boolean open,
+            final OnAvatarPublication callback) {
         final Bundle options;
         if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
-            options = PublishOptions.openAccess();
+            options = open ? PublishOptions.openAccess() : PublishOptions.presenceAccess();
         } else {
             options = null;
         }
@@ -4886,7 +4893,7 @@ public class XmppConnectionService extends Service {
                 });
     }
 
-    public void republishAvatarIfNeeded(Account account) {
+    public void republishAvatarIfNeeded(final Account account) {
         if (account.getAxolotlService().isPepBroken()) {
             Log.d(
                     Config.LOGTAG,
@@ -4922,17 +4929,21 @@ public class XmppConnectionService extends Service {
                     @Override
                     public void accept(final Iq packet) {
                         if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) {
-                            Avatar serverAvatar = parseAvatar(packet);
+                            final Avatar serverAvatar = parseAvatar(packet);
                             if (serverAvatar == null && account.getAvatar() != null) {
-                                Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar());
+                                final Avatar avatar =
+                                        fileBackend.getStoredPepAvatar(account.getAvatar());
                                 if (avatar != null) {
                                     Log.d(
                                             Config.LOGTAG,
                                             account.getJid().asBareJid()
                                                     + ": avatar on server was null. republishing");
+                                    // publishing as 'open' - old server (that requires
+                                    // republication) likely doesn't support access models anyway
                                     publishAvatar(
                                             account,
                                             fileBackend.getStoredPepAvatar(account.getAvatar()),
+                                            true,
                                             null);
                                 } else {
                                     Log.e(

src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java 🔗

@@ -11,19 +11,12 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnLongClickListener;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
 import android.widget.Toast;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
 import androidx.databinding.DataBindingUtil;
-
 import com.canhub.cropper.CropImage;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding;
@@ -31,83 +24,89 @@ import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
 import eu.siacs.conversations.utils.PhoneHelper;
+import java.util.concurrent.atomic.AtomicBoolean;
 
-public class PublishProfilePictureActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication {
+public class PublishProfilePictureActivity extends XmppActivity
+        implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication {
 
     public static final int REQUEST_CHOOSE_PICTURE = 0x1337;
 
-    private ImageView avatar;
-    private TextView hintOrWarning;
-    private TextView secondaryHint;
-    private Button cancelButton;
-    private Button publishButton;
+    private ActivityPublishProfilePictureBinding binding;
     private Uri avatarUri;
     private Uri defaultUri;
     private Account account;
     private boolean support = false;
     private boolean publishing = false;
     private final AtomicBoolean handledExternalUri = new AtomicBoolean(false);
-    private final OnLongClickListener backToDefaultListener = new OnLongClickListener() {
-
-        @Override
-        public boolean onLongClick(View v) {
-            avatarUri = defaultUri;
-            loadImageIntoPreview(defaultUri);
-            return true;
-        }
-    };
+    private final OnLongClickListener backToDefaultListener =
+            new OnLongClickListener() {
+
+                @Override
+                public boolean onLongClick(View v) {
+                    avatarUri = defaultUri;
+                    loadImageIntoPreview(defaultUri);
+                    return true;
+                }
+            };
     private boolean mInitialAccountSetup;
 
     @Override
     public void onAvatarPublicationSucceeded() {
-        runOnUiThread(() -> {
-            if (mInitialAccountSetup) {
-                Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class);
-                StartConversationActivity.addInviteUri(intent, getIntent());
-                intent.putExtra("init", true);
-                intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
-                startActivity(intent);
-            }
-            Toast.makeText(PublishProfilePictureActivity.this,
-                    R.string.avatar_has_been_published,
-                    Toast.LENGTH_SHORT).show();
-            finish();
-        });
+        runOnUiThread(
+                () -> {
+                    if (mInitialAccountSetup) {
+                        Intent intent =
+                                new Intent(
+                                        getApplicationContext(), StartConversationActivity.class);
+                        StartConversationActivity.addInviteUri(intent, getIntent());
+                        intent.putExtra("init", true);
+                        intent.putExtra(
+                                EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
+                        startActivity(intent);
+                    }
+                    Toast.makeText(
+                                    PublishProfilePictureActivity.this,
+                                    R.string.avatar_has_been_published,
+                                    Toast.LENGTH_SHORT)
+                            .show();
+                    finish();
+                });
     }
 
     @Override
-    public void onAvatarPublicationFailed(int res) {
-        runOnUiThread(() -> {
-            hintOrWarning.setText(res);
-            hintOrWarning.setVisibility(View.VISIBLE);
-            publishing = false;
-            togglePublishButton(true, R.string.publish);
-        });
+    public void onAvatarPublicationFailed(final int res) {
+        runOnUiThread(
+                () -> {
+                    this.binding.hintOrWarning.setText(res);
+                    this.binding.hintOrWarning.setVisibility(View.VISIBLE);
+                    this.publishing = false;
+                    togglePublishButton(true, R.string.publish);
+                });
     }
 
     @Override
-    public void onCreate(Bundle savedInstanceState) {
+    public void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        ActivityPublishProfilePictureBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture);
+        this.binding =
+                DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture);
 
         setSupportActionBar(binding.toolbar);
 
         Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
 
-        this.avatar = findViewById(R.id.account_image);
-        this.cancelButton = findViewById(R.id.cancel_button);
-        this.publishButton = findViewById(R.id.publish_button);
-        this.hintOrWarning = findViewById(R.id.hint_or_warning);
-        this.secondaryHint = findViewById(R.id.secondary_hint);
-        this.publishButton.setOnClickListener(v -> {
-            if (avatarUri != null) {
-                publishing = true;
-                togglePublishButton(false, R.string.publishing);
-                xmppConnectionService.publishAvatar(account, avatarUri, this);
-            }
-        });
-        this.cancelButton.setOnClickListener(
+        this.binding.publishButton.setOnClickListener(
+                v -> {
+                    final boolean open = !this.binding.contactOnly.isChecked();
+                    final var uri = this.avatarUri;
+                    if (uri == null) {
+                        return;
+                    }
+                    publishing = true;
+                    togglePublishButton(false, R.string.publishing);
+                    xmppConnectionService.publishAvatarAsync(account, uri, open, this);
+                });
+        this.binding.cancelButton.setOnClickListener(
                 v -> {
                     if (mInitialAccountSetup) {
                         final Intent intent =
@@ -126,11 +125,12 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
                     }
                     finish();
                 });
-        this.avatar.setOnClickListener(v -> chooseAvatar(this));
+        this.binding.accountImage.setOnClickListener(v -> chooseAvatar(this));
         this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext());
         if (savedInstanceState != null) {
             this.avatarUri = savedInstanceState.getParcelable("uri");
-            this.handledExternalUri.set(savedInstanceState.getBoolean("handle_external_uri",false));
+            this.handledExternalUri.set(
+                    savedInstanceState.getBoolean("handle_external_uri", false));
         }
     }
 
@@ -143,8 +143,8 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
     @Override
     public boolean onOptionsItemSelected(final MenuItem item) {
         if (item.getItemId() == R.id.action_delete_avatar) {
-            if (xmppConnectionService != null && account != null) {
-                xmppConnectionService.deleteAvatar(account);
+            if (account != null) {
+                deleteAvatar(account);
             }
             return true;
         } else {
@@ -152,6 +152,22 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
         }
     }
 
+    private void deleteAvatar(final Account account) {
+        new MaterialAlertDialogBuilder(this)
+                .setTitle(R.string.delete_avatar)
+                .setMessage(R.string.delete_avatar_message)
+                .setNegativeButton(R.string.cancel, null)
+                .setPositiveButton(
+                        R.string.confirm,
+                        (d, v) -> {
+                            if (xmppConnectionService != null) {
+                                xmppConnectionService.deleteAvatar(account);
+                            }
+                        })
+                .create()
+                .show();
+    }
+
     @Override
     public void onSaveInstanceState(@NonNull Bundle outState) {
         if (this.avatarUri != null) {
@@ -161,7 +177,6 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
         super.onSaveInstanceState(outState);
     }
 
-
     @Override
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
@@ -190,9 +205,9 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
             final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
             intent.setType("image/*");
             activity.startActivityForResult(
-                    Intent.createChooser(intent, activity.getString(R.string.attach_choose_picture)),
-                    REQUEST_CHOOSE_PICTURE
-            );
+                    Intent.createChooser(
+                            intent, activity.getString(R.string.attach_choose_picture)),
+                    REQUEST_CHOOSE_PICTURE);
         } else {
             CropImage.activity()
                     .setOutputCompressFormat(Bitmap.CompressFormat.PNG)
@@ -211,7 +226,9 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
     }
 
     private void reloadAvatar() {
-        this.support = this.account.getXmppConnection() != null && this.account.getXmppConnection().getFeatures().pep();
+        this.support =
+                this.account.getXmppConnection() != null
+                        && this.account.getXmppConnection().getFeatures().pep();
         if (this.avatarUri == null) {
             if (this.account.getAvatar() != null || this.defaultUri == null) {
                 loadImageIntoPreview(null);
@@ -232,69 +249,82 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
 
         final Uri uri = intent != null ? intent.getData() : null;
 
-        if (uri != null && handledExternalUri.compareAndSet(false,true)) {
+        if (uri != null && handledExternalUri.compareAndSet(false, true)) {
             cropUri(this, uri);
             return;
         }
 
         if (this.mInitialAccountSetup) {
-            this.cancelButton.setText(R.string.skip);
+            this.binding.cancelButton.setText(R.string.skip);
         }
-        configureActionBar(getSupportActionBar(), !this.mInitialAccountSetup && !handledExternalUri.get());
+        configureActionBar(
+                getSupportActionBar(), !this.mInitialAccountSetup && !handledExternalUri.get());
     }
 
     public static void cropUri(final Activity activity, final Uri uri) {
-        CropImage.activity(uri).setOutputCompressFormat(Bitmap.CompressFormat.PNG)
+        CropImage.activity(uri)
+                .setOutputCompressFormat(Bitmap.CompressFormat.PNG)
                 .setAspectRatio(1, 1)
                 .setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE)
                 .start(activity);
     }
 
-    protected void loadImageIntoPreview(Uri uri) {
+    protected void loadImageIntoPreview(final Uri uri) {
 
         Bitmap bm = null;
         if (uri == null) {
-            bm = avatarService().get(account, (int) getResources().getDimension(R.dimen.publish_avatar_size));
+            bm =
+                    avatarService()
+                            .get(
+                                    account,
+                                    (int) getResources().getDimension(R.dimen.publish_avatar_size));
         } else {
             try {
-                bm = xmppConnectionService.getFileBackend().cropCenterSquare(uri, (int) getResources().getDimension(R.dimen.publish_avatar_size));
-            } catch (Exception e) {
+                bm =
+                        xmppConnectionService
+                                .getFileBackend()
+                                .cropCenterSquare(
+                                        uri,
+                                        (int)
+                                                getResources()
+                                                        .getDimension(R.dimen.publish_avatar_size));
+            } catch (final Exception e) {
                 Log.d(Config.LOGTAG, "unable to load bitmap into image view", e);
             }
         }
 
         if (bm == null) {
             togglePublishButton(false, R.string.publish);
-            this.hintOrWarning.setVisibility(View.VISIBLE);
-            this.hintOrWarning.setText(R.string.error_publish_avatar_converting);
+            this.binding.hintOrWarning.setVisibility(View.VISIBLE);
+            this.binding.hintOrWarning.setText(R.string.error_publish_avatar_converting);
             return;
         }
-        this.avatar.setImageBitmap(bm);
+        this.binding.accountImage.setImageBitmap(bm);
         if (support) {
             togglePublishButton(uri != null, R.string.publish);
-            this.hintOrWarning.setVisibility(View.INVISIBLE);
+            this.binding.hintOrWarning.setVisibility(View.INVISIBLE);
         } else {
             togglePublishButton(false, R.string.publish);
-            this.hintOrWarning.setVisibility(View.VISIBLE);
+            this.binding.hintOrWarning.setVisibility(View.VISIBLE);
             if (account.getStatus() == Account.State.ONLINE) {
-                this.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support);
+                this.binding.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support);
             } else {
-                this.hintOrWarning.setText(R.string.error_publish_avatar_offline);
+                this.binding.hintOrWarning.setText(R.string.error_publish_avatar_offline);
             }
         }
         if (this.defaultUri == null || this.defaultUri.equals(uri)) {
-            this.secondaryHint.setVisibility(View.INVISIBLE);
-            this.avatar.setOnLongClickListener(null);
+            this.binding.secondaryHint.setVisibility(View.INVISIBLE);
+            this.binding.accountImage.setOnLongClickListener(null);
         } else if (this.defaultUri != null) {
-            this.secondaryHint.setVisibility(View.VISIBLE);
-            this.avatar.setOnLongClickListener(this.backToDefaultListener);
+            this.binding.secondaryHint.setVisibility(View.VISIBLE);
+            this.binding.accountImage.setOnLongClickListener(this.backToDefaultListener);
         }
     }
 
     protected void togglePublishButton(boolean enabled, @StringRes int res) {
         final boolean status = enabled && !publishing;
-        this.publishButton.setText(publishing ? R.string.publishing : res);
-        this.publishButton.setEnabled(status);
+        this.binding.publishButton.setText(publishing ? R.string.publishing : res);
+        this.binding.publishButton.setEnabled(status);
     }
 
     public void refreshUiReal() {
@@ -307,5 +337,4 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
     public void onAccountUpdate() {
         refreshUi();
     }
-
 }

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

@@ -1,16 +1,13 @@
 package eu.siacs.conversations.xmpp.pep;
 
 import android.os.Bundle;
-
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import im.conversations.android.xmpp.model.stanza.Iq;
 
 public class PublishOptions {
 
-    private PublishOptions() {
-
-    }
+    private PublishOptions() {}
 
     public static Bundle openAccess() {
         final Bundle options = new Bundle();
@@ -18,6 +15,12 @@ public class PublishOptions {
         return options;
     }
 
+    public static Bundle presenceAccess() {
+        final Bundle options = new Bundle();
+        options.putString("pubsub#access_model", "presence");
+        return options;
+    }
+
     public static Bundle persistentWhitelistAccess() {
         final Bundle options = new Bundle();
         options.putString("pubsub#persist_items", "true");
@@ -32,14 +35,15 @@ public class PublishOptions {
         options.putString("pubsub#send_last_published_item", "never");
         options.putString("pubsub#max_items", "max");
         options.putString("pubsub#notify_delete", "true");
-        options.putString("pubsub#notify_retract", "true"); //one could also set notify=true on the retract
+        options.putString(
+                "pubsub#notify_retract", "true"); // one could also set notify=true on the retract
 
         return options;
     }
 
     public static boolean preconditionNotMet(Iq response) {
-        final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null;
+        final Element error =
+                response.getType() == Iq.Type.ERROR ? response.findChild("error") : null;
         return error != null && error.hasChild("precondition-not-met", Namespace.PUBSUB_ERROR);
     }
-
 }

src/main/res/layout/activity_publish_profile_picture.xml 🔗

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android">
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
 
     <RelativeLayout
         android:layout_width="match_parent"
@@ -19,18 +20,19 @@
         </com.google.android.material.appbar.AppBarLayout>
 
 
-        <LinearLayout
-            android:layout_width="fill_parent"
+        <ScrollView
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_below="@id/app_bar_layout"
-            android:layout_marginLeft="@dimen/activity_horizontal_margin"
-            android:layout_marginTop="@dimen/activity_vertical_margin"
-            android:layout_marginRight="@dimen/activity_horizontal_margin"
-            android:layout_marginBottom="@dimen/activity_vertical_margin">
+            android:layout_above="@+id/button_bar"
+            android:layout_below="@id/app_bar_layout">
 
             <LinearLayout
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/activity_horizontal_margin"
+                android:layout_marginTop="@dimen/activity_vertical_margin"
+                android:layout_marginRight="@dimen/activity_horizontal_margin"
+                android:layout_marginBottom="@dimen/activity_vertical_margin"
                 android:gravity="center_horizontal"
                 android:orientation="vertical">
 
@@ -63,17 +65,25 @@
                     android:text="@string/or_long_press_for_default"
                     android:textAppearance="?textAppearanceBodyMedium" />
 
+                <com.google.android.material.materialswitch.MaterialSwitch
+                    android:id="@+id/contact_only"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginVertical="12dp"
+                    android:text="@string/show_to_contacts_only" />
+
                 <TextView
                     android:id="@+id/hint_or_warning"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:layout_marginTop="8dp"
-                    android:layout_marginBottom="8dp"
+                    android:layout_marginVertical="12dp"
                     android:textAppearance="?textAppearanceBodyMedium"
-                    android:textColor="?colorError" />
-
+                    android:textColor="?colorError"
+                    tools:text="@string/error_saving_avatar" />
             </LinearLayout>
-        </LinearLayout>
+
+
+        </ScrollView>
 
         <RelativeLayout
             android:id="@+id/button_bar"

src/main/res/values/strings.xml 🔗

@@ -1099,4 +1099,6 @@
     <string name="pref_align_start_summary">Display all messages, including sent ones, on the left side for a uniform chat layout.</string>
     <string name="custom_notifications">Custom notifications</string>
     <string name="custom_notifications_enable">Enable customized notification settings (importance, sound, vibration) settings for this conversation?</string>
+    <string name="delete_avatar_message">Would you like to delete your avatar? Some clients might continue to display a cached copy of your avatar.</string>
+    <string name="show_to_contacts_only">Show to contacts only</string>
 </resources>