From d0397114c1d38c060218a9220e69ee7287e8b5b8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 23 Dec 2024 17:24:03 +0100 Subject: [PATCH] add option to restrict avatar access model to contacts --- .../services/XmppConnectionService.java | 65 +++--- .../ui/PublishProfilePictureActivity.java | 207 ++++++++++-------- .../xmpp/pep/PublishOptions.java | 18 +- .../activity_publish_profile_picture.xml | 36 +-- src/main/res/values/strings.xml | 2 + 5 files changed, 192 insertions(+), 136 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 81e6b361da6e50a7f938174117b5033b3c795079..2762f29e0aa0f03156538108d075ed4a1971d9d1 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/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( diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index 2c972734cf3c87b1cbf0358bc6594b5743674b76..dafbbabfc5564c94cdeb9c15d013726900b5b70f 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/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(); } - } diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java index ee3770ead9f3a56322ba8fd62c4660b7f954b701..c9b764752d3bbea3225ffa4b67bb1a3a75b31730 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java +++ b/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); } - } diff --git a/src/main/res/layout/activity_publish_profile_picture.xml b/src/main/res/layout/activity_publish_profile_picture.xml index 0eb4dd516ca6e8af33a5682fe388476650859e1e..6516e432ce4a9ddd1c3e87f1d124da04a2dea662 100644 --- a/src/main/res/layout/activity_publish_profile_picture.xml +++ b/src/main/res/layout/activity_publish_profile_picture.xml @@ -1,5 +1,6 @@ - + - + android:layout_above="@+id/button_bar" + android:layout_below="@id/app_bar_layout"> @@ -63,17 +65,25 @@ android:text="@string/or_long_press_for_default" android:textAppearance="?textAppearanceBodyMedium" /> + + - + android:textColor="?colorError" + tools:text="@string/error_saving_avatar" /> - + + + Display all messages, including sent ones, on the left side for a uniform chat layout. Custom notifications Enable customized notification settings (importance, sound, vibration) settings for this conversation? + Would you like to delete your avatar? Some clients might continue to display a cached copy of your avatar. + Show to contacts only