WIP: set room avatar and slight redesign of group details

This commit is contained in:
Daniel Gultsch 2018-06-18 14:15:19 +02:00
parent f434925753
commit d7ebd7d453
14 changed files with 725 additions and 467 deletions

View File

@ -35,7 +35,7 @@ dependencies {
exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-core'
} }
implementation 'org.sufficientlysecure:openpgp-api:10.0' 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:support-v13:$supportLibVersion"
implementation "com.android.support:appcompat-v7:$supportLibVersion" implementation "com.android.support:appcompat-v7:$supportLibVersion"
implementation "com.android.support:cardview-v7:$supportLibVersion" implementation "com.android.support:cardview-v7:$supportLibVersion"

View File

@ -180,6 +180,7 @@
android:windowSoftInputMode="stateHidden|adjustResize"/> android:windowSoftInputMode="stateHidden|adjustResize"/>
<activity <activity
android:name=".ui.ConferenceDetailsActivity" android:name=".ui.ConferenceDetailsActivity"
android:label="@string/action_muc_details"
android:windowSoftInputMode="stateHidden"/> android:windowSoftInputMode="stateHidden"/>
<activity <activity
android:name=".ui.ContactDetailsActivity" android:name=".ui.ContactDetailsActivity"
@ -188,6 +189,9 @@
android:name=".ui.PublishProfilePictureActivity" android:name=".ui.PublishProfilePictureActivity"
android:label="@string/mgmt_account_publish_avatar" android:label="@string/mgmt_account_publish_avatar"
android:windowSoftInputMode="stateHidden"/> android:windowSoftInputMode="stateHidden"/>
<activity
android:name=".ui.PublishGroupChatProfilePictureActivity"
android:label="@string/group_chat_avatar"/>
<activity <activity
android:name=".ui.ShareWithActivity" android:name=".ui.ShareWithActivity"
android:label="@string/app_name" android:label="@string/app_name"
@ -234,7 +238,8 @@
<category android:name="android.intent.category.PREFERENCE"/> <category android:name="android.intent.category.PREFERENCE"/>
</intent-filter> </intent-filter>
</activity> </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"/> <activity android:name=".ui.MemorizingActivity"/>
<service android:name=".services.ExportLogsService"/> <service android:name=".services.ExportLogsService"/>

View File

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

View File

@ -736,6 +736,15 @@ public class FileBackend {
} }
public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { 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); Bitmap bm = cropCenterSquare(image, size);
if (bm == null) { if (bm == null) {
return null; return null;
@ -749,6 +758,19 @@ public class FileBackend {
return getPepAvatar(bm, format, 100); 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) { private Avatar getPepAvatar(Bitmap bitmap, Bitmap.CompressFormat format, int quality) {
try { try {
ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();

View File

@ -60,6 +60,8 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import javax.security.auth.callback.Callback;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OmemoSetting; 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.persistance.FileBackend;
import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
import eu.siacs.conversations.utils.ConversationsFileObserver; import eu.siacs.conversations.utils.ConversationsFileObserver;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
@ -2446,18 +2449,21 @@ public class XmppConnectionService extends Service {
public void onIqPacketReceived(Account account, IqPacket packet) { public void onIqPacketReceived(Account account, IqPacket packet) {
Element query = packet.findChild("query", "http://jabber.org/protocol/disco#info"); Element query = packet.findChild("query", "http://jabber.org/protocol/disco#info");
if (packet.getType() == IqPacket.TYPE.RESULT && query != null) { if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
String name = null;
ArrayList<String> features = new ArrayList<>(); ArrayList<String> features = new ArrayList<>();
for (Element child : query.getChildren()) { for (Element child : query.getChildren()) {
if (child != null && child.getName().equals("feature")) { if (child.getName().equals("feature")) {
String var = child.getAttribute("var"); String var = child.getAttribute("var");
if (var != null) { if (var != null) {
features.add(var); features.add(var);
} }
} else if (child.getName().equals("identity")) {
name = child.getAttribute("name");
} }
} }
Element form = query.findChild("x", Namespace.DATA); Element form = query.findChild("x", Namespace.DATA);
Data data = form == null ? null : Data.parse(form); 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()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid());
updateConversation(conversation); 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(() -> { new Thread(() -> {
final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
final int size = Config.AVATAR_SIZE; final int size = Config.AVATAR_SIZE;
final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
if (avatar != null) { if (avatar != null) {
if (!getFileBackend().save(avatar)) { 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; return;
} }
publishAvatar(account, avatar, callback); publishAvatar(account, avatar, callback);
} else { } else {
callback.error(R.string.error_publish_avatar_converting, null); callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
} }
}).start(); }).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); IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
this.sendIqPacket(account, packet, new OnIqPacketReceived() { 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"); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": published avatar " + (avatar.size / 1024) + "KiB");
if (callback != null) { if (callback != null) {
callback.success(avatar); callback.onAvatarPublicationSucceeded();
} }
} else { } else {
if (callback != null) { 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"); Element error = result.findChild("error");
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server rejected avatar " + (avatar.size / 1024) + "KiB " + (error != null ? error.toString() : "")); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server rejected avatar " + (avatar.size / 1024) + "KiB " + (error != null ? error.toString() : ""));
if (callback != null) { if (callback != null) {
callback.error(R.string.error_publish_avatar_server_reject, avatar); callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
} }
} }
} }

View File

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

View File

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

View File

@ -1,16 +1,11 @@
package eu.siacs.conversations.ui; package eu.siacs.conversations.ui;
import android.app.PendingIntent;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.util.Log; import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnLongClickListener; import android.view.View.OnLongClickListener;
import android.widget.Button; import android.widget.Button;
@ -18,293 +13,217 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.soundcloud.android.crop.Crop; import com.theartofdev.edmodo.cropper.CropImage;
import java.io.File;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService; 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.utils.PhoneHelper;
import eu.siacs.conversations.xmpp.pep.Avatar;
public class PublishProfilePictureActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate { public class PublishProfilePictureActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication {
private static final int REQUEST_CHOOSE_FILE_AND_CROP = 0xac23; private ImageView avatar;
private static final int REQUEST_CHOOSE_FILE = 0xac24; private TextView hintOrWarning;
private ImageView avatar; private TextView secondaryHint;
private TextView hintOrWarning; private Button cancelButton;
private TextView secondaryHint; private Button publishButton;
private Button cancelButton; private Uri avatarUri;
private Button publishButton; private Uri defaultUri;
private Uri avatarUri; private Account account;
private Uri defaultUri; private boolean support = false;
private Account account; private boolean publishing = false;
private boolean support = false; private OnLongClickListener backToDefaultListener = new OnLongClickListener() {
private boolean publishing = false;
private OnLongClickListener backToDefaultListener = new OnLongClickListener() {
@Override @Override
public boolean onLongClick(View v) { public boolean onLongClick(View v) {
avatarUri = defaultUri; avatarUri = defaultUri;
loadImageIntoPreview(defaultUri); loadImageIntoPreview(defaultUri);
return true; return true;
} }
}; };
private boolean mInitialAccountSetup; private boolean mInitialAccountSetup;
private UiCallback<Avatar> avatarPublication = new UiCallback<Avatar>() {
@Override @Override
public void success(Avatar object) { public void onAvatarPublicationSucceeded() {
runOnUiThread(() -> { runOnUiThread(() -> {
if (mInitialAccountSetup) { if (mInitialAccountSetup) {
Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class); Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class);
WelcomeActivity.addInviteUri(intent, getIntent()); WelcomeActivity.addInviteUri(intent, getIntent());
intent.putExtra("init", true); intent.putExtra("init", true);
startActivity(intent); startActivity(intent);
} }
Toast.makeText(PublishProfilePictureActivity.this, Toast.makeText(PublishProfilePictureActivity.this,
R.string.avatar_has_been_published, R.string.avatar_has_been_published,
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
finish(); finish();
}); });
} }
@Override @Override
public void error(final int errorCode, Avatar object) { public void onAvatarPublicationFailed(int res) {
runOnUiThread(() -> { runOnUiThread(() -> {
hintOrWarning.setText(errorCode); hintOrWarning.setText(res);
hintOrWarning.setTextColor(getWarningTextColor()); hintOrWarning.setTextColor(getWarningTextColor());
hintOrWarning.setVisibility(View.VISIBLE); hintOrWarning.setVisibility(View.VISIBLE);
publishing = false; publishing = false;
togglePublishButton(true,R.string.publish); 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());
@Override this.avatar = findViewById(R.id.account_image);
public void userInputRequried(PendingIntent pi, Avatar object) { 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 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); @Override
this.cancelButton = findViewById(R.id.cancel_button); public void onActivityResult(int requestCode, int resultCode, Intent data) {
this.publishButton = findViewById(R.id.publish_button); if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
this.hintOrWarning = findViewById(R.id.hint_or_warning); CropImage.ActivityResult result = CropImage.getActivityResult(data);
this.secondaryHint = findViewById(R.id.secondary_hint); if (resultCode == RESULT_OK) {
this.publishButton.setOnClickListener(v -> { this.avatarUri = result.getUri();
if (avatarUri != null) { if (xmppConnectionServiceBound) {
publishing = true; loadImageIntoPreview(this.avatarUri);
togglePublishButton(false,R.string.publishing); }
xmppConnectionService.publishAvatar(account, avatarUri, avatarPublication); } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
} Exception error = result.getError();
}); if (error != null) {
this.cancelButton.setOnClickListener(v -> { Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show();
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);
}
}); private void chooseAvatar() {
this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext()); CropImage.activity()
} .setOutputCompressFormat(Bitmap.CompressFormat.PNG)
.setAspectRatio(1, 1)
.setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE)
.start(this);
}
private void chooseAvatar(boolean crop) { @Override
Intent attachFileIntent = new Intent(); protected void onBackendConnected() {
attachFileIntent.setType("image/*"); this.account = extractAccount(getIntent());
attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); if (this.account != null) {
Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file)); reloadAvatar();
startActivityForResult(chooser, crop ? REQUEST_CHOOSE_FILE_AND_CROP : REQUEST_CHOOSE_FILE); }
} }
@Override private void reloadAvatar() {
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { this.support = this.account.getXmppConnection() != null && this.account.getXmppConnection().getFeatures().pep();
if (grantResults.length > 0) if (this.avatarUri == null) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (this.account.getAvatar() != null || this.defaultUri == null) {
if (requestCode == REQUEST_CHOOSE_FILE_AND_CROP) { loadImageIntoPreview(null);
chooseAvatar(true); } else {
} else if (requestCode == REQUEST_CHOOSE_FILE) { this.avatarUri = this.defaultUri;
chooseAvatar(false); loadImageIntoPreview(this.defaultUri);
} }
} else { } else {
Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); loadImageIntoPreview(avatarUri);
} }
} }
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { protected void onStart() {
getMenuInflater().inflate(R.menu.publish_avatar, menu); super.onStart();
return super.onCreateOptionsMenu(menu); if (getIntent() != null) {
} this.mInitialAccountSetup = getIntent().getBooleanExtra("setup", false);
}
if (this.mInitialAccountSetup) {
this.cancelButton.setText(R.string.skip);
}
}
@Override protected void loadImageIntoPreview(Uri uri) {
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 Bitmap bm = null;
protected void onActivityResult(int requestCode, int resultCode, final Intent data) { if (uri == null) {
super.onActivityResult(requestCode, resultCode, data); bm = avatarService().get(account, getPixel(192));
if (resultCode == RESULT_OK) { } else {
Uri source = data.getData(); try {
switch (requestCode) { bm = xmppConnectionService.getFileBackend().cropCenterSquare(uri, getPixel(192));
case REQUEST_CHOOSE_FILE_AND_CROP: } catch (Exception e) {
if (FileBackend.weOwnFile(this, source)) { Log.d(Config.LOGTAG, "unable to load bitmap into image view", e);
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 if (bm == null) {
protected void onBackendConnected() { togglePublishButton(false, R.string.publish);
this.account = extractAccount(getIntent()); this.hintOrWarning.setVisibility(View.VISIBLE);
if (this.account != null) { this.hintOrWarning.setTextColor(getWarningTextColor());
reloadAvatar(); 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);
}
}
private void reloadAvatar() { protected void togglePublishButton(boolean enabled, @StringRes int res) {
this.support = this.account.getXmppConnection() != null && this.account.getXmppConnection().getFeatures().pep(); final boolean status = enabled && !publishing;
if (this.avatarUri == null) { this.publishButton.setText(publishing ? R.string.publishing : res);
if (this.account.getAvatar() != null || this.defaultUri == null) { this.publishButton.setEnabled(status);
loadImageIntoPreview(null); }
} else {
this.avatarUri = this.defaultUri;
loadImageIntoPreview(this.defaultUri);
}
} else {
loadImageIntoPreview(avatarUri);
}
}
@Override public void refreshUiReal() {
protected void onStart() { if (this.account != null) {
super.onStart(); reloadAvatar();
if (getIntent() != null) { }
this.mInitialAccountSetup = getIntent().getBooleanExtra("setup", false); }
}
if (this.mInitialAccountSetup) {
this.cancelButton.setText(R.string.skip);
}
}
protected void loadImageIntoPreview(Uri uri) { @Override
public void onAccountUpdate() {
refreshUi();
}
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();
}
} }

View File

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

View File

@ -8,8 +8,9 @@
android:background="?attr/color_background_secondary" android:background="?attr/color_background_secondary"
android:orientation="vertical"> android:orientation="vertical">
<include android:id="@+id/toolbar" <include
layout="@layout/toolbar" /> android:id="@+id/toolbar"
layout="@layout/toolbar"/>
<ScrollView <ScrollView
android:layout_width="fill_parent" android:layout_width="fill_parent"
@ -35,14 +36,6 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/card_padding_regular"> 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 <RelativeLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -50,44 +43,52 @@
<com.makeramen.roundedimageview.RoundedImageView <com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/your_photo" android:id="@+id/your_photo"
android:layout_width="48dp" android:layout_width="72dp"
android:layout_height="48dp" android:layout_height="72dp"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
app:riv_corner_radius="2dp"/> app:riv_corner_radius="2dp"/>
<LinearLayout <LinearLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_alignParentTop="true"
android:layout_toRightOf="@+id/your_photo" android:layout_toRightOf="@+id/your_photo"
android:orientation="vertical" android:orientation="vertical"
android:paddingLeft="@dimen/avatar_item_distance"> android:paddingLeft="@dimen/avatar_item_distance">
<TextView <RelativeLayout
android:id="@+id/muc_your_nick" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Conversations.Subhead"/>
<TextView <TextView
android:id="@+id/muc_role" android:id="@+id/muc_title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:singleLine="true"
android:textAppearance="@style/TextAppearance.Conversations.Body1.Secondary"/> 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> </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>
<RelativeLayout <RelativeLayout
@ -120,35 +121,6 @@
android:src="?attr/icon_settings"/> android:src="?attr/icon_settings"/>
</RelativeLayout> </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 <TableLayout
android:id="@+id/muc_info_more" android:id="@+id/muc_info_more"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -179,6 +151,88 @@
</TableRow> </TableRow>
</TableLayout> </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 <TextView
android:id="@+id/details_account" android:id="@+id/details_account"
@ -193,7 +247,7 @@
<android.support.v7.widget.CardView <android.support.v7.widget.CardView
android:id="@+id/muc_more_details" android:id="@+id/muc_more_details"
android:layout_width="fill_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_vertical_margin" android:layout_marginBottom="@dimen/activity_vertical_margin"
android:layout_marginLeft="@dimen/activity_horizontal_margin" android:layout_marginLeft="@dimen/activity_horizontal_margin"

View File

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

View File

@ -2,13 +2,6 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> 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 <item
android:id="@+id/action_share" android:id="@+id/action_share"
android:icon="?attr/icon_share" android:icon="?attr/icon_share"

View File

@ -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>

View File

@ -510,7 +510,6 @@
<string name="correct_message">Correct message</string> <string name="correct_message">Correct message</string>
<string name="send_corrected_message">Send corrected 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="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="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="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> <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="p1_s3_filetransfer">HTTP File Sharing for S3</string>
<string name="pref_start_search">Direct Search</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="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> </resources>