add option to restrict avatar access model to contacts

This commit is contained in:
Daniel Gultsch 2024-12-23 17:24:03 +01:00 committed by Arne
parent a555141178
commit ee33f3f6c9
5 changed files with 185 additions and 127 deletions

View file

@ -4577,23 +4577,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(Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
@ -4631,10 +4640,14 @@ public class XmppConnectionService extends Service {
});
}
public void publishAvatar(Account account, final Avatar avatar, final OnAvatarPublication callback) {
public void publishAvatar(
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;
}
@ -4706,7 +4719,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, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken");
return;
@ -4735,12 +4748,19 @@ 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");
publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null);
// 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(Config.LOGTAG, account.getJid().asBareJid() + ": error rereading avatar");
}

View file

@ -14,19 +14,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;
@ -35,89 +28,97 @@ import eu.siacs.conversations.persistance.FileBackend;
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() {
private final OnLongClickListener backToDefaultListener =
new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
avatarUri = defaultUri;
loadImageIntoPreview(defaultUri);
return true;
}
};
@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 =
new Intent(
getApplicationContext(), StartConversationActivity.class);
intent.putExtra("init", true);
if (xmppConnectionService != null
&& xmppConnectionService.getAccounts().size() == 1) {
intent.putExtra("init", true);
}
StartConversationActivity.addInviteUri(intent, getIntent());
if (account != null) {
intent.putExtra(
@ -127,11 +128,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));
}
}
@ -144,8 +146,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 {
@ -153,6 +155,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) {
@ -191,9 +209,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)
@ -212,7 +230,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);
@ -233,21 +253,22 @@ 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(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 void cropUri(final Uri uri) {
if (Build.VERSION.SDK_INT >= 28) {
loadImageIntoPreview(uri);
if (this.avatar.getDrawable() instanceof AnimatedImageDrawable || this.avatar.getDrawable() instanceof FileBackend.SVGDrawable) {
if (this.binding.accountImage.getDrawable() instanceof AnimatedImageDrawable || this.binding.accountImage.getDrawable() instanceof FileBackend.SVGDrawable) {
this.avatarUri = uri;
return;
}
@ -259,7 +280,7 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
.start(this);
}
protected void loadImageIntoPreview(Uri uri) {
protected void loadImageIntoPreview(final Uri uri) {
Drawable bm = null;
if (uri == null) {
@ -267,46 +288,46 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
} else {
try {
bm = xmppConnectionService.getFileBackend().cropCenterSquareDrawable(uri, (int) getResources().getDimension(R.dimen.publish_avatar_size));
} catch (Exception e) {
} 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.setImageDrawable(bm);
this.binding.accountImage.setImageDrawable(bm);
if (Build.VERSION.SDK_INT >= 28 && bm instanceof AnimatedImageDrawable) {
((AnimatedImageDrawable) bm).start();
}
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() {

View file

@ -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);
}
}

View file

@ -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">
@ -62,17 +64,26 @@
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"

View file

@ -1408,4 +1408,6 @@
<string name="pref_prefer_ipv6">Prefer IPv6</string>
<string name="pref_use_colored_muc_names">Use colored user names</string>
<string name="pref_use_colored_muc_names_summary">Use colored user names in group chats</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>