publish avatars

iNPUTmice created

Change summary

res/layout/activity_publish_profile_picture.xml                  | 11 
res/values/strings.xml                                           |  2 
src/eu/siacs/conversations/generator/AbstractGenerator.java      |  3 
src/eu/siacs/conversations/generator/IqGenerator.java            | 31 
src/eu/siacs/conversations/parser/MessageParser.java             | 25 
src/eu/siacs/conversations/persistance/FileBackend.java          |  2 
src/eu/siacs/conversations/services/Defaults.java                | 11 
src/eu/siacs/conversations/services/XmppConnectionService.java   | 44 
src/eu/siacs/conversations/ui/PublishProfilePictureActivity.java | 89 +
src/eu/siacs/conversations/xml/Element.java                      |  8 
src/eu/siacs/conversations/xmpp/pep/Avatar.java                  |  3 
src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java       |  3 
12 files changed, 197 insertions(+), 35 deletions(-)

Detailed changes

res/layout/activity_publish_profile_picture.xml 🔗

@@ -75,13 +75,20 @@
         android:paddingLeft="8dp"
         android:paddingRight="8dp" >
 
+        <TextView 
+            android:id="@+id/account"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/primarytext"
+            android:textSize="18sp"/>
+        
         <TextView
-            android:id="@+id/explanation"
+            android:layout_marginTop="8dp"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="@string/publish_avatar_explanation"
             android:textColor="@color/primarytext"
-            android:textSize="18sp" />
+            android:textSize="14sp" />
     </LinearLayout>
 
 </RelativeLayout>

res/values/strings.xml 🔗

@@ -270,5 +270,5 @@
     <string name="contact_has_read_up_to_this_point">%s has read up to this point</string>
     <string name="publish_avatar">Publish avatar</string>
     <string name="touch_to_choose_picture">Touch avatar to select picture from gallary</string>
-    <string name="publish_avatar_explanation">Publish avatar for <b>%s</b>. Everyone subscribed to your presence updates will also be able to see this picture.</string>
+    <string name="publish_avatar_explanation">Please note: Everyone subscribed to your presence updates will be allowed to see this picture.</string>
 </resources>

src/eu/siacs/conversations/generator/AbstractGenerator.java 🔗

@@ -18,7 +18,8 @@ public abstract class AbstractGenerator {
 			"http://jabber.org/protocol/muc",
 			"jabber:x:conference",
 			"http://jabber.org/protocol/caps",
-			"http://jabber.org/protocol/disco#info"};
+			"http://jabber.org/protocol/disco#info",
+			"urn:xmpp:avatar:metadata+notify"};
 	public final String IDENTITY_NAME = "Conversations 0.5";
 	public final String IDENTITY_TYPE = "phone";
 	/*public final String[] FEATURES = { "http://jabber.org/protocol/muc","http://jabber.org/protocol/disco#info", "http://jabber.org/protocol/disco#items", "http://jabber.org/protocol/caps" };

src/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -5,6 +5,7 @@ import java.util.Collections;
 import java.util.List;
 
 import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.pep.Avatar;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 
 public class IqGenerator extends AbstractGenerator {
@@ -28,4 +29,34 @@ public class IqGenerator extends AbstractGenerator {
 		}
 		return packet;
 	}
+	
+	protected IqPacket publish(String node, Element item) {
+		IqPacket packet = new IqPacket(IqPacket.TYPE_SET);
+		Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub");
+		Element publish = pubsub.addChild("publish");
+		publish.setAttribute("node", node);
+		publish.addChild(item);
+		return packet;
+	}
+	
+	public IqPacket publishAvatar(Avatar avatar) {
+		Element item = new Element("item");
+		item.setAttribute("id", avatar.sha1sum);
+		Element data = item.addChild("data","urn:xmpp:avatar:data");
+		data.setContent(avatar.image);
+		return publish("urn:xmpp:avatar:data", item);
+	}
+	
+	public IqPacket publishAvatarMetadata(Avatar avatar) {
+		Element item = new Element("item");
+		item.setAttribute("id", avatar.sha1sum);
+		Element metadata = item.addChild("metadata","urn:xmpp:avatar:metadata");
+		Element info = metadata.addChild("info");
+		info.setAttribute("bytes",avatar.size);
+		info.setAttribute("id",avatar.sha1sum);
+		info.setAttribute("height",avatar.height);
+		info.setAttribute("width",avatar.height);
+		info.setAttribute("type", avatar.type);
+		return publish("urn:xmpp:avatar:metadata",item);
+	}
 }

src/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -1,6 +1,7 @@
 package eu.siacs.conversations.parser;
 
 import android.os.SystemClock;
+import android.util.Log;
 import net.java.otr4j.session.Session;
 import net.java.otr4j.session.SessionStatus;
 import eu.siacs.conversations.entities.Account;
@@ -213,6 +214,10 @@ public class MessageParser extends AbstractParser implements
 	}
 
 	private void parseNormal(Element packet, Account account) {
+		if (packet.hasChild("event","http://jabber.org/protocol/pubsub#event")) {
+			Element event = packet.findChild("event","http://jabber.org/protocol/pubsub#event");
+			parseEvent(event,packet.getAttribute("from"),account);
+		}
 		if (packet.hasChild("displayed", "urn:xmpp:chat-markers:0")) {
 			String id = packet
 					.findChild("displayed", "urn:xmpp:chat-markers:0")
@@ -254,6 +259,16 @@ public class MessageParser extends AbstractParser implements
 		}
 	}
 
+	private void parseEvent(Element event, String from, Account account) {
+		Element items = event.findChild("items");
+		String node = items.getAttribute("node");
+		if (node!=null) {
+			Log.d("xmppService",account.getJid()+": "+node+" from "+from);
+		} else {
+			Log.d("xmppService",event.toString());
+		}
+	}
+
 	private String getPgpBody(Element message) {
 		Element child = message.findChild("x", "jabber:x:encrypted");
 		if (child == null) {
@@ -324,6 +339,9 @@ public class MessageParser extends AbstractParser implements
 		} else if (packet.getType() == MessagePacket.TYPE_NORMAL) {
 			this.parseNormal(packet, account);
 			return;
+		} else if (packet.getType() == MessagePacket.TYPE_HEADLINE) {
+			this.parseHeadline(packet, account);
+			return;
 		}
 		if ((message == null) || (message.getBody() == null)) {
 			return;
@@ -346,4 +364,11 @@ public class MessageParser extends AbstractParser implements
 		}
 		mXmppConnectionService.notifyUi(conversation, notify);
 	}
+
+	private void parseHeadline(MessagePacket packet, Account account) {
+		if (packet.hasChild("event","http://jabber.org/protocol/pubsub#event")) {
+			Element event = packet.findChild("event","http://jabber.org/protocol/pubsub#event");
+			parseEvent(event,packet.getFrom(),account);
+		}
+	}
 }

src/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -261,7 +261,7 @@ public class FileBackend {
 			mDigestOutputStream.write(avatar.getImageAsBytes());
 			mDigestOutputStream.flush();
 			mDigestOutputStream.close();
-			Log.d("xmppService","sha1sum after write: "+CryptoHelper.bytesToHex(digest.digest()));
+			avatar.size = file.length();
 		} catch (FileNotFoundException e) {
 			
 		} catch (IOException e) {

src/eu/siacs/conversations/services/Defaults.java 🔗

@@ -0,0 +1,11 @@
+package eu.siacs.conversations.services;
+
+import android.graphics.Bitmap;
+
+public final class Defaults {
+	public static final int AVATAR_SIZE = 192;
+	public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;
+	private Defaults() {
+		
+	}
+}

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

@@ -20,6 +20,7 @@ import de.duenndns.ssl.MemorizingTrustManager;
 import net.java.otr4j.OtrException;
 import net.java.otr4j.session.Session;
 import net.java.otr4j.session.SessionStatus;
+import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.PgpEngine;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Bookmark;
@@ -1186,13 +1187,46 @@ public class XmppConnectionService extends Service {
 	}
 
 	
-	public void pushAvatar(Account account, Uri image) {
-		Avatar avatar = getFileBackend().getPepAvatar(image, 192, Bitmap.CompressFormat.WEBP);
+	public void publishAvatar(Account account, Uri image, final UiCallback<Avatar> callback) {
+		final Bitmap.CompressFormat format = Defaults.AVATAR_FORMAT;
+		final int size = Defaults.AVATAR_SIZE;
+		final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
 		if (avatar!=null) {
-			Log.d(LOGTAG,avatar.sha1sum);
-			Log.d(LOGTAG,avatar.image);
-			avatar.type = "image/webp";
+			avatar.height = size;
+			avatar.width = size;
+			if (format.equals(Bitmap.CompressFormat.WEBP)) {
+				avatar.type = "image/webp";
+			} else if (format.equals(Bitmap.CompressFormat.JPEG)) {
+				avatar.type = "image/jpeg";
+			} else if (format.equals(Bitmap.CompressFormat.PNG)) {
+				avatar.type = "image/png";
+			}
 			getFileBackend().save(avatar);
+			IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
+			this.sendIqPacket(account, packet, new OnIqPacketReceived() {
+				
+				@Override
+				public void onIqPacketReceived(Account account, IqPacket result) {
+					if (result.getType() == IqPacket.TYPE_RESULT) {
+						IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar);
+						sendIqPacket(account, packet, new OnIqPacketReceived() {
+							
+							@Override
+							public void onIqPacketReceived(Account account, IqPacket result) {
+								if (result.getType() == IqPacket.TYPE_RESULT) {
+									callback.success(avatar);
+								} else {
+									callback.error(R.string.error, avatar);
+								}
+							}
+						});
+					} else {
+						callback.error(R.string.error, avatar);
+					}
+				}
+			});
+		} else {
+			callback.error(R.string.error, null);
 		}
 	}
 	

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

@@ -1,10 +1,11 @@
 package eu.siacs.conversations.ui;
 
+import android.app.PendingIntent;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
-import android.util.Log;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Button;
@@ -13,47 +14,69 @@ import android.widget.TextView;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.xmpp.pep.Avatar;
 
 public class PublishProfilePictureActivity extends XmppActivity {
-	
+
 	private static final int REQUEST_CHOOSE_FILE = 0xac23;
-	
+
 	private ImageView avatar;
-	private TextView explanation;
+	private TextView accountTextView;
 	private Button cancelButton;
 	private Button publishButton;
-	
+
 	private Uri avatarUri;
-	
+
 	private Account account;
-	
+
+	private UiCallback<Avatar> avatarPublication = new UiCallback<Avatar>() {
+
+		@Override
+		public void success(Avatar object) {
+			finish();
+		}
+
+		@Override
+		public void error(int errorCode, Avatar object) {
+			// TODO Auto-generated method stub
+
+		}
+
+		@Override
+		public void userInputRequried(PendingIntent pi, Avatar object) {
+			// TODO Auto-generated method stub
+
+		}
+	};
+
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		setContentView(R.layout.activity_publish_profile_picture);
 		this.avatar = (ImageView) findViewById(R.id.account_image);
-		this.explanation = (TextView) findViewById(R.id.explanation);
 		this.cancelButton = (Button) findViewById(R.id.cancel_button);
 		this.publishButton = (Button) findViewById(R.id.publish_button);
+		this.accountTextView = (TextView) findViewById(R.id.account);
 		this.publishButton.setOnClickListener(new OnClickListener() {
-			
+
 			@Override
 			public void onClick(View v) {
-				if (avatarUri!=null) {
-					xmppConnectionService.pushAvatar(account, avatarUri);
-					finish();
+				if (avatarUri != null) {
+					disablePublishButton();
+					xmppConnectionService.publishAvatar(account, avatarUri,
+							avatarPublication);
 				}
 			}
 		});
 		this.cancelButton.setOnClickListener(new OnClickListener() {
-			
+
 			@Override
 			public void onClick(View v) {
 				finish();
 			}
 		});
 		this.avatar.setOnClickListener(new OnClickListener() {
-			
+
 			@Override
 			public void onClick(View v) {
 				Intent attachFileIntent = new Intent();
@@ -65,45 +88,61 @@ public class PublishProfilePictureActivity extends XmppActivity {
 			}
 		});
 	}
-	
+
 	@Override
 	protected void onActivityResult(int requestCode, int resultCode,
 			final Intent data) {
 		super.onActivityResult(requestCode, resultCode, data);
 		if (resultCode == RESULT_OK) {
 			if (requestCode == REQUEST_CHOOSE_FILE) {
-				Log.d("xmppService","bla");
 				this.avatarUri = data.getData();
 			}
 		}
 	}
+	
+	@Override
+	public boolean onOptionsItemSelected(MenuItem menuItem) {
+		super.onOptionsItemSelected(menuItem);
+		switch (menuItem.getItemId()) {
+		case android.R.id.home:
+			finish();
+			break;
+		}
+		return true;
+	}
 
 	@Override
 	protected void onBackendConnected() {
-		if (getIntent()!=null) {
+		if (getIntent() != null) {
 			String jid = getIntent().getStringExtra("account");
-			if (jid!=null) {
+			if (jid != null) {
 				this.account = xmppConnectionService.findAccountByJid(jid);
 				if (this.avatarUri == null) {
-					avatarUri = PhoneHelper.getSefliUri(getApplicationContext());
+					avatarUri = PhoneHelper
+							.getSefliUri(getApplicationContext());
 				}
 				loadImageIntoPreview(avatarUri);
-				String explainText = getString(R.string.publish_avatar_explanation,account.getJid());
-				this.explanation.setText(explainText);
+				this.accountTextView.setText(this.account.getJid());
 			}
 		}
-		
+
 	}
-	
+
 	protected void loadImageIntoPreview(Uri uri) {
-		Bitmap bm = xmppConnectionService.getFileBackend().cropCenterSquare(uri, 384);
+		Bitmap bm = xmppConnectionService.getFileBackend().cropCenterSquare(
+				uri, 384);
 		this.avatar.setImageBitmap(bm);
 		enablePublishButton();
 	}
-	
+
 	protected void enablePublishButton() {
 		this.publishButton.setEnabled(true);
 		this.publishButton.setTextColor(getPrimaryTextColor());
 	}
+	
+	protected void disablePublishButton() {
+		this.publishButton.setEnabled(false);
+		this.publishButton.setTextColor(getSecondaryTextColor());
+	}
 
 }

src/eu/siacs/conversations/xml/Element.java 🔗

@@ -144,4 +144,12 @@ public class Element {
 	public void clearChildren() {
 		this.children.clear();
 	}
+
+	public void setAttribute(String name, long value) {
+		this.setAttribute(name, ""+value);
+	}
+	
+	public void setAttribute(String name, int value) {
+		this.setAttribute(name, ""+value);
+	}
 }

src/eu/siacs/conversations/xmpp/pep/Avatar.java 🔗

@@ -6,6 +6,9 @@ public class Avatar {
 	public String type;
 	public String sha1sum;
 	public String image;
+	public int height;
+	public int width;
+	public long size;
 	public byte[] getImageAsBytes() {
 		return Base64.decode(image, Base64.DEFAULT);
 	}

src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java 🔗

@@ -8,6 +8,7 @@ public class MessagePacket extends AbstractStanza {
 	public static final int TYPE_NORMAL = 2;
 	public static final int TYPE_GROUPCHAT = 3;
 	public static final int TYPE_ERROR = 4;
+	public static final int TYPE_HEADLINE = 5;
 	
 	public MessagePacket() {
 		super("message");
@@ -59,6 +60,8 @@ public class MessagePacket extends AbstractStanza {
 			return TYPE_GROUPCHAT;
 		} else if (type.equals("error")) {
 			return TYPE_ERROR;
+		} else if (type.equals("headline")) {
+			return TYPE_HEADLINE;
 		} else {
 			return TYPE_UNKNOWN;
 		}