WIP: set room avatar and slight redesign of group details

Daniel Gultsch created

Change summary

build.gradle                                                                        |   2 
src/main/AndroidManifest.xml                                                        |   7 
src/main/java/eu/siacs/conversations/entities/MucOptions.java                       |  16 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java                   |  22 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java            |  77 
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java              |  41 
src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java | 157 
src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java          | 491 
src/main/java/eu/siacs/conversations/ui/interfaces/OnAvatarPublication.java         |  39 
src/main/res/layout/activity_muc_details.xml                                        | 188 
src/main/res/layout/activity_publish_profile_picture.xml                            | 160 
src/main/res/menu/muc_details.xml                                                   |   7 
src/main/res/menu/publish_avatar.xml                                                |   9 
src/main/res/values/strings.xml                                                     |   4 
14 files changed, 739 insertions(+), 481 deletions(-)

Detailed changes

build.gradle πŸ”—

@@ -35,7 +35,7 @@ dependencies {
         exclude group: 'com.google.firebase', module: 'firebase-core'
     }
     implementation 'org.sufficientlysecure:openpgp-api:10.0'
-    implementation 'com.soundcloud.android:android-crop:1.0.1@aar'
+    implementation 'com.theartofdev.edmodo:android-image-cropper:2.7.+'
     implementation "com.android.support:support-v13:$supportLibVersion"
     implementation "com.android.support:appcompat-v7:$supportLibVersion"
     implementation "com.android.support:cardview-v7:$supportLibVersion"

src/main/AndroidManifest.xml πŸ”—

@@ -180,6 +180,7 @@
             android:windowSoftInputMode="stateHidden|adjustResize"/>
         <activity
             android:name=".ui.ConferenceDetailsActivity"
+            android:label="@string/action_muc_details"
             android:windowSoftInputMode="stateHidden"/>
         <activity
             android:name=".ui.ContactDetailsActivity"
@@ -188,6 +189,9 @@
             android:name=".ui.PublishProfilePictureActivity"
             android:label="@string/mgmt_account_publish_avatar"
             android:windowSoftInputMode="stateHidden"/>
+        <activity
+            android:name=".ui.PublishGroupChatProfilePictureActivity"
+            android:label="@string/group_chat_avatar"/>
         <activity
             android:name=".ui.ShareWithActivity"
             android:label="@string/app_name"
@@ -234,7 +238,8 @@
                 <category android:name="android.intent.category.PREFERENCE"/>
             </intent-filter>
         </activity>
-        <activity android:name="com.soundcloud.android.crop.CropImageActivity"/>
+        <activity android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
+                  android:theme="@style/Base.Theme.AppCompat"/>
         <activity android:name=".ui.MemorizingActivity"/>
 
         <service android:name=".services.ExportLogsService"/>

src/main/java/eu/siacs/conversations/entities/MucOptions.java πŸ”—

@@ -1,6 +1,7 @@
 package eu.siacs.conversations.entities;
 
 import android.annotation.SuppressLint;
+import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -373,7 +374,7 @@ public class MucOptions {
 		this.self = new User(this, createJoinJid(getProposedNick()));
 	}
 
-	public boolean updateConfiguration(List<String> features, Data data) {
+	public boolean updateConfiguration(List<String> features, String name, Data data) {
 		updateFeatures(features);
 		updateFormData(data == null ? new Data() : data);
 		Field allowPmField = this.form.getFieldByName("muc#roomconfig_allowpm");
@@ -382,6 +383,7 @@ public class MucOptions {
 		changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, this.hasFeature("muc_membersonly"));
 		changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MODERATED, this.hasFeature("muc_moderated"));
 		changed |= conversation.setAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, this.hasFeature("muc_nonanonymous"));
+		changed |= setName(name);
 		return changed;
 	}
 
@@ -402,6 +404,10 @@ public class MucOptions {
 		return this.features.contains(feature);
 	}
 
+	public boolean hasVCards() {
+	    return hasFeature("vcard-temp");
+    }
+
 	public boolean canInvite() {
 		Field field = this.form.getFieldByName("muc#roomconfig_allowinvites");
 		return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
@@ -688,6 +694,14 @@ public class MucOptions {
 		return this.conversation.getAttribute("subject");
 	}
 
+	private boolean setName(String name) {
+		return this.conversation.setAttribute("muc_name", name);
+	}
+
+	public String getName() {
+		return this.conversation.getAttribute("muc_name");
+	}
+
 	public List<User> getFallbackUsersFromCryptoTargets() {
 		List<User> users = new ArrayList<>();
 		for (Jid jid : conversation.getAcceptedCryptoTargets()) {

src/main/java/eu/siacs/conversations/persistance/FileBackend.java πŸ”—

@@ -736,6 +736,15 @@ public class FileBackend {
 	}
 
 	public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
+
+		final Avatar uncompressAvatar = getUncompressedAvatar(image);
+		if (uncompressAvatar != null && uncompressAvatar.image.length() <= Config.AVATAR_CHAR_LIMIT) {
+			return uncompressAvatar;
+		}
+		if (uncompressAvatar != null) {
+			Log.d(Config.LOGTAG,"uncompressed avatar exceeded char limit by "+(uncompressAvatar.image.length() - Config.AVATAR_CHAR_LIMIT));
+		}
+
 		Bitmap bm = cropCenterSquare(image, size);
 		if (bm == null) {
 			return null;
@@ -749,6 +758,19 @@ public class FileBackend {
 		return getPepAvatar(bm, format, 100);
 	}
 
+	private Avatar getUncompressedAvatar(Uri uri) {
+		Bitmap bitmap = null;
+		try {
+			bitmap = BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri));
+			return getPepAvatar(bitmap, Bitmap.CompressFormat.PNG, 100);
+		} catch (Exception e) {
+			if (bitmap != null) {
+				bitmap.recycle();
+			}
+		}
+		return null;
+	}
+
 	private Avatar getPepAvatar(Bitmap bitmap, Bitmap.CompressFormat format, int quality) {
 		try {
 			ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java πŸ”—

@@ -60,6 +60,8 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 
+import javax.security.auth.callback.Callback;
+
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.OmemoSetting;
@@ -97,6 +99,7 @@ import eu.siacs.conversations.persistance.DatabaseBackend;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.ui.SettingsActivity;
 import eu.siacs.conversations.ui.UiCallback;
+import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
 import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
 import eu.siacs.conversations.utils.ConversationsFileObserver;
 import eu.siacs.conversations.utils.CryptoHelper;
@@ -2446,18 +2449,21 @@ public class XmppConnectionService extends Service {
 			public void onIqPacketReceived(Account account, IqPacket packet) {
 				Element query = packet.findChild("query", "http://jabber.org/protocol/disco#info");
 				if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
+					String name = null;
 					ArrayList<String> features = new ArrayList<>();
 					for (Element child : query.getChildren()) {
-						if (child != null && child.getName().equals("feature")) {
+						if (child.getName().equals("feature")) {
 							String var = child.getAttribute("var");
 							if (var != null) {
 								features.add(var);
 							}
+						} else if (child.getName().equals("identity")) {
+							name = child.getAttribute("name");
 						}
 					}
 					Element form = query.findChild("x", Namespace.DATA);
 					Data data = form == null ? null : Data.parse(form);
-					if (conversation.getMucOptions().updateConfiguration(features, data)) {
+					if (conversation.getMucOptions().updateConfiguration(features, name, data)) {
 						Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid());
 						updateConversation(conversation);
 					}
@@ -2681,25 +2687,78 @@ public class XmppConnectionService extends Service {
 		}
 	}
 
-	public void publishAvatar(final Account account, final Uri image, final UiCallback<Avatar> callback) {
+	public void publishMucAvatar(final Conversation conversation, 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)) {
-					callback.error(R.string.error_saving_avatar, avatar);
+					callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
+					return;
+				}
+				avatar.owner = conversation.getJid().asBareJid();
+				publishMucAvatar(conversation, avatar, callback);
+			} else {
+				callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
+			}
+		}).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.error(R.string.error_publish_avatar_converting, null);
+				callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
 			}
 		}).start();
 
 	}
 
-	public void publishAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
+	private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
+		final IqPacket retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
+		sendIqPacket(conversation.getAccount(), retrieve, (account, response) -> {
+			boolean itemNotFound = response.getType() == IqPacket.TYPE.ERROR && response.hasChild("error") && response.findChild("error").hasChild("item-not-found");
+			if (response.getType() == IqPacket.TYPE.RESULT || itemNotFound) {
+				Element vcard = response.findChild("vCard", "vcard-temp");
+				if (vcard == null) {
+					vcard = new Element("vCard", "vcard-temp");
+				}
+				Element photo = vcard.findChild("PHOTO");
+				if (photo == null) {
+					photo = vcard.addChild("PHOTO");
+				}
+				photo.clearChildren();
+				photo.addChild("TYPE").setContent(avatar.type);
+				photo.addChild("BINVAL").setContent(avatar.image);
+				IqPacket publication = new IqPacket(IqPacket.TYPE.SET);
+				publication.setTo(conversation.getJid().asBareJid());
+				publication.addChild(vcard);
+				sendIqPacket(account, publication, (a1, publicationResponse) -> {
+					if (publicationResponse.getType() == IqPacket.TYPE.RESULT) {
+						callback.onAvatarPublicationSucceeded();
+					} else {
+						Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getError());
+						callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
+					}
+				});
+			} else {
+				Log.d(Config.LOGTAG, "failed to request vcard " + response.toString());
+				callback.onAvatarPublicationFailed(R.string.error_publish_avatar_no_server_support);
+			}
+		});
+	}
+
+	public void publishAvatar(Account account, final Avatar avatar, final OnAvatarPublication callback) {
 		IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
 		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
 
@@ -2717,11 +2776,11 @@ public class XmppConnectionService extends Service {
 								}
 								Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": published avatar " + (avatar.size / 1024) + "KiB");
 								if (callback != null) {
-									callback.success(avatar);
+									callback.onAvatarPublicationSucceeded();
 								}
 							} else {
 								if (callback != null) {
-									callback.error(R.string.error_publish_avatar_server_reject, avatar);
+									callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
 								}
 							}
 						}
@@ -2730,7 +2789,7 @@ public class XmppConnectionService extends Service {
 					Element error = result.findChild("error");
 					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server rejected avatar " + (avatar.size / 1024) + "KiB " + (error != null ? error.toString() : ""));
 					if (callback != null) {
-						callback.error(R.string.error_publish_avatar_server_reject, avatar);
+						callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
 					}
 				}
 			}

src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java πŸ”—

@@ -2,6 +2,7 @@ package eu.siacs.conversations.ui;
 
 import android.app.PendingIntent;
 import android.content.Context;
+import android.content.Intent;
 import android.content.IntentSender.SendIntentException;
 import android.content.res.Resources;
 import android.databinding.DataBindingUtil;
@@ -12,6 +13,7 @@ import android.os.AsyncTask;
 import android.os.Bundle;
 import android.support.v7.app.AlertDialog;
 import android.support.v7.widget.Toolbar;
+import android.util.Log;
 import android.view.ContextMenu;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -247,6 +249,20 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
 		this.mAdvancedMode = getPreferences().getBoolean("advanced_muc_mode", false);
 		this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
 		this.binding.notificationStatusButton.setOnClickListener(this.mNotifyStatusClickListener);
+		this.binding.yourPhoto.setOnClickListener(v -> {
+			final MucOptions mucOptions = mConversation.getMucOptions();
+			if (!mucOptions.hasVCards()) {
+				Toast.makeText(this,R.string.host_does_not_support_group_chat_avatars, Toast.LENGTH_SHORT).show();
+				return;
+			}
+			if (!mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
+				Toast.makeText(this,R.string.only_the_owner_can_change_group_chat_avatar, Toast.LENGTH_SHORT).show();
+				return;
+			}
+			final Intent intent = new Intent(this, PublishGroupChatProfilePictureActivity.class);
+			intent.putExtra("uuid",mConversation.getUuid());
+			startActivity(intent);
+		});
 	}
 
 	@Override
@@ -267,14 +283,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
 			case android.R.id.home:
 				finish();
 				break;
-			case R.id.action_edit_subject:
-				if (mConversation != null) {
-					quickEdit(mConversation.getMucOptions().getSubject(),
-							R.string.edit_subject_hint,
-							this.onSubjectEdited,
-							true);
-				}
-				break;
 			case R.id.action_share_http:
 				shareLink(true);
 				break;
@@ -318,7 +326,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
 		MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark);
 		MenuItem menuItemDeleteBookmark = menu.findItem(R.id.action_delete_bookmark);
 		MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode);
-		MenuItem menuItemChangeSubject = menu.findItem(R.id.action_edit_subject);
 		menuItemAdvancedMode.setChecked(mAdvancedMode);
 		if (mConversation == null) {
 			return true;
@@ -330,7 +337,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
 			menuItemDeleteBookmark.setVisible(false);
 			menuItemSaveBookmark.setVisible(true);
 		}
-		menuItemChangeSubject.setVisible(mConversation.getMucOptions().canChangeSubject());
 		return true;
 	}
 
@@ -525,21 +531,16 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
 			account = mConversation.getAccount().getJid().asBareJid().toString();
 		}
 		this.binding.detailsAccount.setText(getString(R.string.using_account, account));
-		this.binding.yourPhoto.setImageBitmap(avatarService().get(mConversation.getAccount(), getPixel(48)));
-		setTitle(mConversation.getName());
-		this.binding.mucJabberid.setText(mConversation.getJid().asBareJid().toString());
+		this.binding.yourPhoto.setImageBitmap(avatarService().get(mConversation, getPixel(72)));
+		this.binding.mucTitle.setText(mucOptions.getName());
+		this.binding.mucSubject.setText(mucOptions.getSubject());
 		this.binding.mucYourNick.setText(mucOptions.getActualNick());
 		if (mucOptions.online()) {
 			this.binding.mucMoreDetails.setVisibility(View.VISIBLE);
 			this.binding.mucSettings.setVisibility(View.VISIBLE);
 			this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
-			final String status = getStatus(self);
-			if (status != null) {
-				this.binding.mucRole.setVisibility(View.VISIBLE);
-				this.binding.mucRole.setText(status);
-			} else {
-				this.binding.mucRole.setVisibility(View.GONE);
-			}
+			this.binding.mucRole.setVisibility(View.VISIBLE);
+			this.binding.mucRole.setText(getStatus(self));
 			if (mucOptions.membersOnly()) {
 				this.binding.mucConferenceType.setText(R.string.private_conference);
 			} else {

src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java πŸ”—

@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.ui;
+
+import android.content.Intent;
+import android.databinding.DataBindingUtil;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.StringRes;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import com.theartofdev.edmodo.cropper.CropImage;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
+import eu.siacs.conversations.ui.util.PendingItem;
+
+public class PublishGroupChatProfilePictureActivity extends XmppActivity implements OnAvatarPublication {
+
+    private static final int REQUEST_CHOOSE_FILE = 0xac24;
+
+    private ActivityPublishProfilePictureBinding binding;
+
+    private final PendingItem<String> pendingConversationUuid = new PendingItem<>();
+
+    private Conversation conversation;
+    private Uri uri;
+
+    @Override
+    protected void refreshUiReal() {
+
+    }
+
+    @Override
+    void onBackendConnected() {
+        String uuid = pendingConversationUuid.pop();
+        if (uuid != null) {
+            this.conversation = xmppConnectionService.findConversationByUuid(uuid);
+        }
+        if (this.conversation == null) {
+            return;
+        }
+        reloadAvatar();
+    }
+
+    private void reloadAvatar() {
+        final int size = (int) getResources().getDimension(R.dimen.publish_avatar_size);
+        Bitmap bitmap;
+        if (uri == null) {
+            bitmap = xmppConnectionService.getAvatarService().get(conversation, size);
+        } else {
+            Log.d(Config.LOGTAG, "loading " + uri.toString() + " into preview");
+            bitmap = xmppConnectionService.getFileBackend().cropCenterSquare(uri, size);
+        }
+        this.binding.accountImage.setImageBitmap(bitmap);
+        this.binding.publishButton.setEnabled(uri != null);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture);
+        setSupportActionBar((Toolbar) this.binding.toolbar);
+        configureActionBar(getSupportActionBar());
+        this.binding.cancelButton.setOnClickListener((v) -> this.finish());
+        this.binding.secondaryHint.setVisibility(View.GONE);
+        this.binding.accountImage.setOnClickListener((v) -> this.chooseAvatar());
+        Intent intent = getIntent();
+        String uuid = intent == null ? null : intent.getStringExtra("uuid");
+        if (uuid != null) {
+            pendingConversationUuid.push(uuid);
+        }
+        this.binding.publishButton.setEnabled(uri != null);
+        this.binding.publishButton.setOnClickListener(this::publish);
+    }
+
+
+    private void publish(View view) {
+        xmppConnectionService.publishMucAvatar(conversation, uri, this);
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
+            CropImage.ActivityResult result = CropImage.getActivityResult(data);
+            if (resultCode == RESULT_OK) {
+                this.uri = result.getUri();
+                if (xmppConnectionServiceBound) {
+                    reloadAvatar();
+                }
+            } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
+                Exception error = result.getError();
+                if (error != null) {
+                    Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show();
+                }
+            }
+        }
+    }
+
+    private void chooseAvatar() {
+        CropImage.activity()
+                .setOutputCompressFormat(Bitmap.CompressFormat.PNG)
+                .setAspectRatio(1, 1)
+                .setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE)
+                .start(this);
+    }
+
+    @Override
+    public void onAvatarPublicationSucceeded() {
+        finish();
+    }
+
+    @Override
+    public void onAvatarPublicationFailed(@StringRes int res) {
+        runOnUiThread(() -> {
+            Toast.makeText(this,res,Toast.LENGTH_SHORT).show();
+            this.binding.publishButton.setText(R.string.publish);
+            this.binding.publishButton.setEnabled(true);
+        });
+    }
+}

src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java πŸ”—

@@ -1,16 +1,11 @@
 package eu.siacs.conversations.ui;
 
-import android.app.PendingIntent;
 import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
-import android.support.annotation.NonNull;
 import android.support.annotation.StringRes;
 import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnLongClickListener;
 import android.widget.Button;
@@ -18,293 +13,217 @@ import android.widget.ImageView;
 import android.widget.TextView;
 import android.widget.Toast;
 
-import com.soundcloud.android.crop.Crop;
-
-import java.io.File;
+import com.theartofdev.edmodo.cropper.CropImage;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
 import eu.siacs.conversations.utils.PhoneHelper;
-import eu.siacs.conversations.xmpp.pep.Avatar;
-
-public class PublishProfilePictureActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate {
-
-	private static final int REQUEST_CHOOSE_FILE_AND_CROP = 0xac23;
-	private static final int REQUEST_CHOOSE_FILE = 0xac24;
-	private ImageView avatar;
-	private TextView hintOrWarning;
-	private TextView secondaryHint;
-	private Button cancelButton;
-	private Button publishButton;
-	private Uri avatarUri;
-	private Uri defaultUri;
-	private Account account;
-	private boolean support = false;
-	private boolean publishing = false;
-	private OnLongClickListener backToDefaultListener = new OnLongClickListener() {
-
-		@Override
-		public boolean onLongClick(View v) {
-			avatarUri = defaultUri;
-			loadImageIntoPreview(defaultUri);
-			return true;
-		}
-	};
-	private boolean mInitialAccountSetup;
-	private UiCallback<Avatar> avatarPublication = new UiCallback<Avatar>() {
-
-		@Override
-		public void success(Avatar object) {
-			runOnUiThread(() -> {
-				if (mInitialAccountSetup) {
-					Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class);
-					WelcomeActivity.addInviteUri(intent, getIntent());
-					intent.putExtra("init", true);
-					startActivity(intent);
-				}
-				Toast.makeText(PublishProfilePictureActivity.this,
-						R.string.avatar_has_been_published,
-						Toast.LENGTH_SHORT).show();
-				finish();
-			});
-		}
-
-		@Override
-		public void error(final int errorCode, Avatar object) {
-			runOnUiThread(() -> {
-				hintOrWarning.setText(errorCode);
-				hintOrWarning.setTextColor(getWarningTextColor());
-				hintOrWarning.setVisibility(View.VISIBLE);
-				publishing = false;
-				togglePublishButton(true,R.string.publish);
-			});
-
-		}
-
-		@Override
-		public void userInputRequried(PendingIntent pi, Avatar object) {
-		}
-	};
-
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-		setContentView(R.layout.activity_publish_profile_picture);
-		setSupportActionBar(findViewById(R.id.toolbar));
-		configureActionBar(getSupportActionBar());
-
-		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, avatarPublication);
-			}
-		});
-		this.cancelButton.setOnClickListener(v -> {
-			if (mInitialAccountSetup) {
-				Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class);
-				if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1) {
-					WelcomeActivity.addInviteUri(intent, getIntent());
-					intent.putExtra("init", true);
-				}
-				startActivity(intent);
-			}
-			finish();
-		});
-		this.avatar.setOnClickListener(v -> {
-			if (hasStoragePermission(REQUEST_CHOOSE_FILE)) {
-				chooseAvatar(false);
-			}
-
-		});
-		this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext());
-	}
-
-	private void chooseAvatar(boolean crop) {
-		Intent attachFileIntent = new Intent();
-		attachFileIntent.setType("image/*");
-		attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
-		Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file));
-		startActivityForResult(chooser, crop ? REQUEST_CHOOSE_FILE_AND_CROP : REQUEST_CHOOSE_FILE);
-	}
-
-	@Override
-	public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
-		if (grantResults.length > 0)
-			if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
-				if (requestCode == REQUEST_CHOOSE_FILE_AND_CROP) {
-					chooseAvatar(true);
-				} else if (requestCode == REQUEST_CHOOSE_FILE) {
-					chooseAvatar(false);
-				}
-			} else {
-				Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
-			}
-	}
-
-	@Override
-	public boolean onCreateOptionsMenu(Menu menu) {
-		getMenuInflater().inflate(R.menu.publish_avatar, menu);
-		return super.onCreateOptionsMenu(menu);
-	}
-
-	@Override
-	public boolean onOptionsItemSelected(final MenuItem item) {
-		if (item.getItemId() == R.id.action_crop_image) {
-			if (hasStoragePermission(REQUEST_CHOOSE_FILE_AND_CROP)) {
-				chooseAvatar(true);
-			}
-			return true;
-		} else {
-			return super.onOptionsItemSelected(item);
-		}
-	}
-
-	@Override
-	protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
-		super.onActivityResult(requestCode, resultCode, data);
-		if (resultCode == RESULT_OK) {
-			Uri source = data.getData();
-			switch (requestCode) {
-				case REQUEST_CHOOSE_FILE_AND_CROP:
-					if (FileBackend.weOwnFile(this, source)) {
-						Toast.makeText(this,R.string.security_error_invalid_file_access,Toast.LENGTH_SHORT).show();
-						return;
-					}
-					String original = FileUtils.getPath(this, source);
-					if (original != null) {
-						source = Uri.parse("file://"+original);
-					}
-					Uri destination = Uri.fromFile(new File(getCacheDir(), "croppedAvatar"));
-					final int size = getPixel(192);
-					Crop.of(source, destination).asSquare().withMaxSize(size, size).start(this);
-					break;
-				case REQUEST_CHOOSE_FILE:
-					if (FileBackend.weOwnFile(this, source)) {
-						Toast.makeText(this,R.string.security_error_invalid_file_access,Toast.LENGTH_SHORT).show();
-						return;
-					}
-					this.avatarUri = source;
-					if (xmppConnectionServiceBound) {
-						loadImageIntoPreview(this.avatarUri);
-					}
-					break;
-				case Crop.REQUEST_CROP:
-					this.avatarUri = Uri.fromFile(new File(getCacheDir(), "croppedAvatar"));
-					if (xmppConnectionServiceBound) {
-						loadImageIntoPreview(this.avatarUri);
-					}
-					break;
-			}
-		} else {
-			if (requestCode == Crop.REQUEST_CROP  && data != null) {
-				Throwable throwable = Crop.getError(data);
-				if (throwable != null && throwable instanceof OutOfMemoryError) {
-					Toast.makeText(this,R.string.selection_too_large, Toast.LENGTH_SHORT).show();
-				}
-			}
-		}
-	}
-
-	@Override
-	protected void onBackendConnected() {
-		this.account = extractAccount(getIntent());
-		if (this.account != null) {
-			reloadAvatar();
-		}
-	}
-
-	private void reloadAvatar() {
-		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);
-			} else {
-				this.avatarUri = this.defaultUri;
-				loadImageIntoPreview(this.defaultUri);
-			}
-		} else {
-			loadImageIntoPreview(avatarUri);
-		}
-	}
-
-	@Override
-	protected void onStart() {
-		super.onStart();
-		if (getIntent() != null) {
-			this.mInitialAccountSetup = getIntent().getBooleanExtra("setup", false);
-		}
-		if (this.mInitialAccountSetup) {
-			this.cancelButton.setText(R.string.skip);
-		}
-	}
-
-	protected void loadImageIntoPreview(Uri uri) {
-
-		Bitmap bm = null;
-		if (uri == null) {
-			bm = avatarService().get(account, getPixel(192));
-		} else {
-			try {
-				bm = xmppConnectionService.getFileBackend().cropCenterSquare(uri, getPixel(192));
-			} catch (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.setTextColor(getWarningTextColor());
-			this.hintOrWarning.setText(R.string.error_publish_avatar_converting);
-			return;
-		}
-		this.avatar.setImageBitmap(bm);
-		if (support) {
-			togglePublishButton(uri != null,R.string.publish);
-			this.hintOrWarning.setVisibility(View.INVISIBLE);
-		} else {
-			togglePublishButton(false,R.string.publish);
-			this.hintOrWarning.setVisibility(View.VISIBLE);
-			this.hintOrWarning.setTextColor(getWarningTextColor());
-			if (account.getStatus() == Account.State.ONLINE) {
-				this.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support);
-			} else {
-				this.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);
-		} else if (this.defaultUri != null) {
-			this.secondaryHint.setVisibility(View.VISIBLE);
-			this.avatar.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);
-	}
 
-	public void refreshUiReal() {
-		if (this.account != null) {
-			reloadAvatar();
-		}
-	}
+public class PublishProfilePictureActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication {
+
+    private ImageView avatar;
+    private TextView hintOrWarning;
+    private TextView secondaryHint;
+    private Button cancelButton;
+    private Button publishButton;
+    private Uri avatarUri;
+    private Uri defaultUri;
+    private Account account;
+    private boolean support = false;
+    private boolean publishing = false;
+    private 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);
+                WelcomeActivity.addInviteUri(intent, getIntent());
+                intent.putExtra("init", true);
+                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.setTextColor(getWarningTextColor());
+            hintOrWarning.setVisibility(View.VISIBLE);
+            publishing = false;
+            togglePublishButton(true, R.string.publish);
+        });
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_publish_profile_picture);
+        setSupportActionBar(findViewById(R.id.toolbar));
+        configureActionBar(getSupportActionBar());
+
+        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(v -> {
+            if (mInitialAccountSetup) {
+                Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class);
+                if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1) {
+                    WelcomeActivity.addInviteUri(intent, getIntent());
+                    intent.putExtra("init", true);
+                }
+                startActivity(intent);
+            }
+            finish();
+        });
+        this.avatar.setOnClickListener(v -> chooseAvatar());
+        this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext());
+    }
+
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
+            CropImage.ActivityResult result = CropImage.getActivityResult(data);
+            if (resultCode == RESULT_OK) {
+                this.avatarUri = result.getUri();
+                if (xmppConnectionServiceBound) {
+                    loadImageIntoPreview(this.avatarUri);
+                }
+            } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
+                Exception error = result.getError();
+                if (error != null) {
+                    Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show();
+                }
+            }
+        }
+    }
+
+    private void chooseAvatar() {
+        CropImage.activity()
+                .setOutputCompressFormat(Bitmap.CompressFormat.PNG)
+                .setAspectRatio(1, 1)
+                .setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE)
+                .start(this);
+    }
+
+    @Override
+    protected void onBackendConnected() {
+        this.account = extractAccount(getIntent());
+        if (this.account != null) {
+            reloadAvatar();
+        }
+    }
+
+    private void reloadAvatar() {
+        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);
+            } else {
+                this.avatarUri = this.defaultUri;
+                loadImageIntoPreview(this.defaultUri);
+            }
+        } else {
+            loadImageIntoPreview(avatarUri);
+        }
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        if (getIntent() != null) {
+            this.mInitialAccountSetup = getIntent().getBooleanExtra("setup", false);
+        }
+        if (this.mInitialAccountSetup) {
+            this.cancelButton.setText(R.string.skip);
+        }
+    }
+
+    protected void loadImageIntoPreview(Uri uri) {
+
+        Bitmap bm = null;
+        if (uri == null) {
+            bm = avatarService().get(account, getPixel(192));
+        } else {
+            try {
+                bm = xmppConnectionService.getFileBackend().cropCenterSquare(uri, getPixel(192));
+            } catch (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.setTextColor(getWarningTextColor());
+            this.hintOrWarning.setText(R.string.error_publish_avatar_converting);
+            return;
+        }
+        this.avatar.setImageBitmap(bm);
+        if (support) {
+            togglePublishButton(uri != null, R.string.publish);
+            this.hintOrWarning.setVisibility(View.INVISIBLE);
+        } else {
+            togglePublishButton(false, R.string.publish);
+            this.hintOrWarning.setVisibility(View.VISIBLE);
+            this.hintOrWarning.setTextColor(getWarningTextColor());
+            if (account.getStatus() == Account.State.ONLINE) {
+                this.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support);
+            } else {
+                this.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);
+        } else if (this.defaultUri != null) {
+            this.secondaryHint.setVisibility(View.VISIBLE);
+            this.avatar.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);
+    }
+
+    public void refreshUiReal() {
+        if (this.account != null) {
+            reloadAvatar();
+        }
+    }
+
+    @Override
+    public void onAccountUpdate() {
+        refreshUi();
+    }
 
-	@Override
-	public void onAccountUpdate() {
-		refreshUi();
-	}
 }

src/main/java/eu/siacs/conversations/ui/interfaces/OnAvatarPublication.java πŸ”—

@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.ui.interfaces;
+
+import android.support.annotation.StringRes;
+
+public interface OnAvatarPublication {
+
+    void onAvatarPublicationSucceeded();
+    void onAvatarPublicationFailed(@StringRes int res);
+
+}

src/main/res/layout/activity_muc_details.xml πŸ”—

@@ -8,8 +8,9 @@
         android:background="?attr/color_background_secondary"
         android:orientation="vertical">
 
-        <include android:id="@+id/toolbar"
-            layout="@layout/toolbar" />
+        <include
+            android:id="@+id/toolbar"
+            layout="@layout/toolbar"/>
 
         <ScrollView
             android:layout_width="fill_parent"
@@ -35,14 +36,6 @@
                         android:orientation="vertical"
                         android:padding="@dimen/card_padding_regular">
 
-                        <TextView
-                            android:id="@+id/muc_jabberid"
-                            android:layout_width="wrap_content"
-                            android:layout_height="wrap_content"
-                            android:layout_marginBottom="16dp"
-                            android:text="@string/account_settings_example_jabber_id"
-                            android:textAppearance="@style/TextAppearance.Conversations.Title"/>
-
                         <RelativeLayout
                             android:layout_width="fill_parent"
                             android:layout_height="wrap_content"
@@ -50,44 +43,52 @@
 
                             <com.makeramen.roundedimageview.RoundedImageView
                                 android:id="@+id/your_photo"
-                                android:layout_width="48dp"
-                                android:layout_height="48dp"
+                                android:layout_width="72dp"
+                                android:layout_height="72dp"
                                 android:layout_alignParentLeft="true"
                                 app:riv_corner_radius="2dp"/>
 
                             <LinearLayout
                                 android:layout_width="fill_parent"
                                 android:layout_height="wrap_content"
-                                android:layout_centerVertical="true"
+                                android:layout_alignParentTop="true"
                                 android:layout_toRightOf="@+id/your_photo"
                                 android:orientation="vertical"
                                 android:paddingLeft="@dimen/avatar_item_distance">
 
-                                <TextView
-                                    android:id="@+id/muc_your_nick"
-                                    android:layout_width="wrap_content"
-                                    android:layout_height="wrap_content"
-                                    android:singleLine="true"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Subhead"/>
-
-                                <TextView
-                                    android:id="@+id/muc_role"
-                                    android:layout_width="wrap_content"
-                                    android:layout_height="wrap_content"
-                                    android:singleLine="true"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1.Secondary"/>
+                                <RelativeLayout
+                                    android:layout_width="match_parent"
+                                    android:layout_height="wrap_content">
+
+                                    <TextView
+                                        android:id="@+id/muc_title"
+                                        android:layout_width="wrap_content"
+                                        android:layout_height="wrap_content"
+                                        android:singleLine="true"
+                                        android:layout_alignParentLeft="true"
+                                        android:layout_toLeftOf="@+id/edit_muc_name_button"
+                                        android:textAppearance="@style/TextAppearance.Conversations.Title"/>
+                                    <TextView
+                                        android:id="@+id/muc_subject"
+                                        android:layout_width="wrap_content"
+                                        android:layout_height="wrap_content"
+                                        android:layout_alignParentLeft="true"
+                                        android:layout_below="@+id/muc_title"
+                                        android:layout_toLeftOf="@+id/edit_muc_name_button"
+                                        android:textAppearance="@style/TextAppearance.Conversations.Subhead"/>
+
+                                    <ImageButton
+                                        android:id="@+id/edit_muc_name_button"
+                                        android:layout_width="wrap_content"
+                                        android:layout_height="wrap_content"
+                                        android:layout_alignParentEnd="true"
+                                        android:layout_alignParentTop="true"
+                                        android:alpha="?attr/icon_alpha"
+                                        android:background="?attr/selectableItemBackgroundBorderless"
+                                        android:padding="@dimen/image_button_padding"
+                                        android:src="?attr/icon_edit_body"/>
+                                </RelativeLayout>
                             </LinearLayout>
-
-                            <ImageButton
-                                android:id="@+id/edit_nick_button"
-                                android:layout_width="wrap_content"
-                                android:layout_height="wrap_content"
-                                android:layout_alignParentRight="true"
-                                android:layout_centerVertical="true"
-                                android:alpha="?attr/icon_alpha"
-                                android:background="?attr/selectableItemBackgroundBorderless"
-                                android:padding="@dimen/image_button_padding"
-                                android:src="?attr/icon_edit_body"/>
                         </RelativeLayout>
 
                         <RelativeLayout
@@ -120,35 +121,6 @@
                                 android:src="?attr/icon_settings"/>
                         </RelativeLayout>
 
-                        <RelativeLayout
-                            android:layout_width="fill_parent"
-                            android:layout_height="wrap_content">
-
-                            <TextView
-                                android:id="@+id/notification_status_text"
-                                android:layout_width="wrap_content"
-                                android:layout_height="wrap_content"
-                                android:layout_alignParentLeft="true"
-                                android:layout_centerVertical="true"
-                                android:layout_toLeftOf="@+id/notification_status_button"
-                                android:text="@string/notify_on_all_messages"
-                                android:textAppearance="@style/TextAppearance.Conversations.Body1"
-                                />
-
-                            <ImageButton
-                                android:id="@+id/notification_status_button"
-                                style="?android:attr/buttonStyleSmall"
-                                android:layout_width="wrap_content"
-                                android:layout_height="wrap_content"
-                                android:layout_alignParentRight="true"
-                                android:layout_centerVertical="true"
-                                android:layout_gravity="center_horizontal"
-                                android:alpha="?attr/icon_alpha"
-                                android:background="?attr/selectableItemBackgroundBorderless"
-                                android:padding="@dimen/image_button_padding"
-                                android:src="?attr/icon_notifications"/>
-                        </RelativeLayout>
-
                         <TableLayout
                             android:id="@+id/muc_info_more"
                             android:layout_width="match_parent"
@@ -179,6 +151,88 @@
                             </TableRow>
 
                         </TableLayout>
+                    </LinearLayout>
+                </android.support.v7.widget.CardView>
+
+                <android.support.v7.widget.CardView
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="@dimen/activity_vertical_margin"
+                    android:layout_marginLeft="@dimen/activity_horizontal_margin"
+                    android:layout_marginRight="@dimen/activity_horizontal_margin"
+                    android:layout_marginTop="@dimen/activity_vertical_margin">
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:orientation="vertical"
+                        android:padding="@dimen/card_padding_regular">
+
+                        <RelativeLayout
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content">
+
+                            <LinearLayout
+                                android:layout_width="fill_parent"
+                                android:layout_height="wrap_content"
+                                android:layout_centerVertical="true"
+                                android:orientation="vertical">
+
+                                <TextView
+                                    android:id="@+id/muc_your_nick"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:singleLine="true"
+                                    android:textAppearance="@style/TextAppearance.Conversations.Subhead"/>
+
+                                <TextView
+                                    android:id="@+id/muc_role"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:singleLine="true"
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1.Secondary"/>
+                            </LinearLayout>
+
+                            <ImageButton
+                                android:id="@+id/edit_nick_button"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:layout_alignParentRight="true"
+                                android:layout_centerVertical="true"
+                                android:alpha="?attr/icon_alpha"
+                                android:background="?attr/selectableItemBackgroundBorderless"
+                                android:padding="@dimen/image_button_padding"
+                                android:src="?attr/icon_edit_body"/>
+                        </RelativeLayout>
+
+                        <RelativeLayout
+                            android:layout_width="fill_parent"
+                            android:layout_height="wrap_content">
+
+                            <TextView
+                                android:id="@+id/notification_status_text"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:layout_alignParentLeft="true"
+                                android:layout_centerVertical="true"
+                                android:layout_toLeftOf="@+id/notification_status_button"
+                                android:text="@string/notify_on_all_messages"
+                                android:textAppearance="@style/TextAppearance.Conversations.Body1"
+                                />
+
+                            <ImageButton
+                                android:id="@+id/notification_status_button"
+                                style="?android:attr/buttonStyleSmall"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:layout_alignParentRight="true"
+                                android:layout_centerVertical="true"
+                                android:layout_gravity="center_horizontal"
+                                android:alpha="?attr/icon_alpha"
+                                android:background="?attr/selectableItemBackgroundBorderless"
+                                android:padding="@dimen/image_button_padding"
+                                android:src="?attr/icon_notifications"/>
+                        </RelativeLayout>
 
                         <TextView
                             android:id="@+id/details_account"
@@ -193,7 +247,7 @@
 
                 <android.support.v7.widget.CardView
                     android:id="@+id/muc_more_details"
-                    android:layout_width="fill_parent"
+                    android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_marginBottom="@dimen/activity_vertical_margin"
                     android:layout_marginLeft="@dimen/activity_horizontal_margin"

src/main/res/layout/activity_publish_profile_picture.xml πŸ”—

@@ -1,97 +1,99 @@
 <?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:background="?attr/color_background_secondary">
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <include layout="@layout/toolbar" />
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="?attr/color_background_secondary">
 
-    <android.support.v7.widget.CardView
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:layout_below="@id/toolbar"
-        android:layout_marginBottom="@dimen/activity_vertical_margin"
-        android:layout_marginLeft="@dimen/activity_horizontal_margin"
-        android:layout_marginRight="@dimen/activity_horizontal_margin"
-        android:layout_marginTop="@dimen/activity_vertical_margin">
+        <include android:id="@+id/toolbar" layout="@layout/toolbar"/>
 
-        <LinearLayout
-            android:layout_width="match_parent"
+        <android.support.v7.widget.CardView
+            android:layout_width="fill_parent"
             android:layout_height="wrap_content"
-            android:gravity="center_horizontal"
-            android:orientation="vertical">
+            android:layout_below="@id/toolbar"
+            android:layout_marginBottom="@dimen/activity_vertical_margin"
+            android:layout_marginLeft="@dimen/activity_horizontal_margin"
+            android:layout_marginRight="@dimen/activity_horizontal_margin"
+            android:layout_marginTop="@dimen/activity_vertical_margin">
 
-            <FrameLayout
-                android:id="@+id/account_image_wrapper"
-                android:layout_width="wrap_content"
+            <LinearLayout
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginBottom="8dp"
-                android:layout_marginTop="@dimen/publish_avatar_top_margin"
-                android:background="@drawable/account_image_border">
+                android:gravity="center_horizontal"
+                android:orientation="vertical">
 
-                <ImageView
-                    android:id="@+id/account_image"
-                    android:layout_width="@dimen/publish_avatar_size"
-                    android:layout_height="@dimen/publish_avatar_size"/>
-            </FrameLayout>
+                <FrameLayout
+                    android:id="@+id/account_image_wrapper"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="8dp"
+                    android:layout_marginTop="@dimen/publish_avatar_top_margin"
+                    android:background="@drawable/account_image_border">
 
-            <TextView
-                android:id="@+id/hint"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/touch_to_choose_picture"
-                android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                    <ImageView
+                        android:id="@+id/account_image"
+                        android:layout_width="@dimen/publish_avatar_size"
+                        android:layout_height="@dimen/publish_avatar_size"/>
+                </FrameLayout>
 
-            <TextView
-                android:id="@+id/secondary_hint"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:textAppearance="@style/TextAppearance.Conversations.Body1"
-                android:text="@string/or_long_press_for_default"/>
+                <TextView
+                    android:id="@+id/hint"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="@string/touch_to_choose_picture"
+                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
 
-            <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:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                <TextView
+                    android:id="@+id/secondary_hint"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="@string/or_long_press_for_default"
+                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
 
-        </LinearLayout>
-    </android.support.v7.widget.CardView>
+                <TextView
+                    android:id="@+id/hint_or_warning"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="8dp"
+                    android:layout_marginTop="8dp"
+                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
 
-    <LinearLayout
-        android:id="@+id/button_bar"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentBottom="true"
-        android:layout_alignParentLeft="true"
-        android:layout_alignParentRight="true">
+            </LinearLayout>
+        </android.support.v7.widget.CardView>
 
-        <Button
-            android:id="@+id/cancel_button"
-            style="@style/Widget.Conversations.Button.Borderless"
-            android:layout_width="0dp"
+        <LinearLayout
+            android:id="@+id/button_bar"
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:text="@string/cancel"/>
+            android:layout_alignParentBottom="true"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentRight="true">
 
-        <View
-            android:layout_width="1dp"
-            android:layout_height="fill_parent"
-            android:layout_marginBottom="7dp"
-            android:layout_marginTop="7dp"
-            android:background="?attr/divider"/>
+            <Button
+                android:id="@+id/cancel_button"
+                style="@style/Widget.Conversations.Button.Borderless"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="@string/cancel"/>
 
-        <Button
-            android:id="@+id/publish_button"
-            style="@style/Widget.Conversations.Button.Borderless"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:enabled="false"
-            android:text="@string/publish"/>
-    </LinearLayout>
+            <View
+                android:layout_width="1dp"
+                android:layout_height="fill_parent"
+                android:layout_marginBottom="7dp"
+                android:layout_marginTop="7dp"
+                android:background="?attr/divider"/>
+
+            <Button
+                android:id="@+id/publish_button"
+                style="@style/Widget.Conversations.Button.Borderless"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:enabled="false"
+                android:text="@string/publish"/>
+        </LinearLayout>
 
-</RelativeLayout>
+    </RelativeLayout>
+</layout>

src/main/res/menu/muc_details.xml πŸ”—

@@ -2,13 +2,6 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto">
 
-    <item
-        android:id="@+id/action_edit_subject"
-        android:icon="?attr/icon_edit"
-        android:orderInCategory="10"
-        app:showAsAction="always"
-        android:title="@string/action_edit_subject"/>
-
     <item
         android:id="@+id/action_share"
         android:icon="?attr/icon_share"

src/main/res/menu/publish_avatar.xml πŸ”—

@@ -1,9 +0,0 @@
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-      xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <item
-        android:id="@+id/action_crop_image"
-        app:showAsAction="always"
-        android:icon="@drawable/ic_crop_white_24dp"
-        android:title="@string/select_image_and_crop"/>
-</menu>

src/main/res/values/strings.xml πŸ”—

@@ -510,7 +510,6 @@
     <string name="correct_message">Correct message</string>
     <string name="send_corrected_message">Send corrected message</string>
     <string name="no_keys_just_confirm">You already trust this contact. By selecting \'done\' you are just confirming that %s is part of this group chat.</string>
-    <string name="select_image_and_crop">Select image and crop</string>
     <string name="this_account_is_disabled">You have disabled this account</string>
     <string name="security_error_invalid_file_access">Security error: Invalid file access</string>
     <string name="no_application_to_share_uri">No application found to share URI</string>
@@ -717,4 +716,7 @@
     <string name="p1_s3_filetransfer">HTTP File Sharing for S3</string>
     <string name="pref_start_search">Direct Search</string>
     <string name="pref_start_search_summary">At β€˜Start Conversation’ screen open keyboard and place cursor in search field</string>
+    <string name="group_chat_avatar">Group chat avatar</string>
+    <string name="host_does_not_support_group_chat_avatars">Host does not support group chat avatars</string>
+    <string name="only_the_owner_can_change_group_chat_avatar">Only the owner can change group chat avatar</string>
 </resources>