activity for future avatar publications. not working yet

iNPUTmice created

Change summary

res/layout/activity_publish_profile_picture.xml                  |  87 +
res/layout/manage_accounts.xml                                   |   3 
res/menu/manageaccounts_context.xml                              |   4 
res/values/strings.xml                                           |   3 
src/eu/siacs/conversations/persistance/FileBackend.java          | 105 +
src/eu/siacs/conversations/services/XmppConnectionService.java   |  13 
src/eu/siacs/conversations/ui/ManageAccountActivity.java         |   2 
src/eu/siacs/conversations/ui/PublishProfilePictureActivity.java | 101 +
src/eu/siacs/conversations/utils/PhoneHelper.java                |   2 
src/eu/siacs/conversations/xmpp/pep/Avatar.java                  |  23 
10 files changed, 340 insertions(+), 3 deletions(-)

Detailed changes

res/layout/activity_publish_profile_picture.xml 🔗

@@ -0,0 +1,87 @@
+<?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"
+    android:background="@color/primarybackground" >
+
+    <LinearLayout
+        android:id="@+id/account_image_wrapper"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentTop="true"
+        android:layout_centerHorizontal="true"
+        android:layout_marginBottom="8dp"
+        android:layout_marginTop="24dp"
+        android:background="@drawable/message_border" >
+
+        <ImageView
+            android:id="@+id/account_image"
+            android:layout_width="194dp"
+            android:layout_height="194dp" />
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/hint"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/account_image_wrapper"
+        android:layout_centerHorizontal="true"
+        android:text="@string/touch_to_choose_picture"
+        android:textColor="@color/secondarytext" />
+
+    <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" >
+
+        <Button
+            android:id="@+id/cancel_button"
+            style="?android:attr/borderlessButtonStyle"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="@string/cancel"
+            android:textColor="@color/primarytext" />
+
+        <View
+            android:layout_width="1dp"
+            android:layout_height="fill_parent"
+            android:layout_marginBottom="7dp"
+            android:layout_marginTop="7dp"
+            android:background="@color/divider" />
+
+        <Button
+            android:id="@+id/publish_button"
+            style="?android:attr/borderlessButtonStyle"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:enabled="false"
+            android:text="@string/publish_avatar"
+            android:textColor="@color/secondarytext" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="fill_parent"
+        android:layout_above="@+id/button_bar"
+        android:layout_below="@+id/hint"
+        android:layout_centerHorizontal="true"
+        android:gravity="center_vertical"
+        android:orientation="vertical"
+        android:paddingLeft="8dp"
+        android:paddingRight="8dp" >
+
+        <TextView
+            android:id="@+id/explanation"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/publish_avatar_explanation"
+            android:textColor="@color/primarytext"
+            android:textSize="18sp" />
+    </LinearLayout>
+
+</RelativeLayout>

res/layout/manage_accounts.xml 🔗

@@ -2,7 +2,8 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="fill_parent"
-    android:layout_height="fill_parent" >
+    android:layout_height="fill_parent"
+    android:background="@color/primarybackground">
 
     <ListView
         android:id="@+id/account_list"

res/menu/manageaccounts_context.xml 🔗

@@ -24,6 +24,10 @@
         android:id="@+id/mgmt_account_announce_pgp"
         android:showAsAction="never"
         android:title="@string/announce_pgp"/>
+    <item
+        android:id="@+id/mgmt_account_publish_avatar"
+        android:showAsAction="never"
+        android:title="@string/publish_avatar"/>
     <item
         android:id="@+id/mgmt_otr_key"
         android:showAsAction="never"

res/values/strings.xml 🔗

@@ -268,4 +268,7 @@
     <string name="contact_added_you">Contact added you to contact list</string>
     <string name="add_back">Add back</string>
     <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>
 </resources>

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

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.persistance;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -7,19 +9,28 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
 import android.graphics.Matrix;
+import android.graphics.RectF;
 import android.media.ExifInterface;
 import android.net.Uri;
+import android.util.Base64;
+import android.util.Base64OutputStream;
 import android.util.Log;
 import android.util.LruCache;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.xmpp.jingle.JingleFile;
+import eu.siacs.conversations.xmpp.pep.Avatar;
 
 public class FileBackend {
 
@@ -215,10 +226,102 @@ public class FileBackend {
 	public File getIncomingFile() {
 		return new File(context.getFilesDir().getAbsolutePath() + "/incoming");
 	}
-	
+
 	public Uri getIncomingUri() {
 		return Uri.parse(context.getFilesDir().getAbsolutePath() + "/incoming");
 	}
+	
+	public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
+		try {
+			Avatar avatar = new Avatar();
+			Bitmap bm = cropCenterSquare(image, size);
+			ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
+			Base64OutputStream mBase64OutputSttream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
+			MessageDigest digest = MessageDigest.getInstance("SHA-1");
+			DigestOutputStream mDigestOutputStream = new DigestOutputStream(mBase64OutputSttream, digest);
+			bm.compress(format, 75, mDigestOutputStream);
+			avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
+			avatar.image = new String(mByteArrayOutputStream.toByteArray());
+			return avatar;
+		} catch (NoSuchAlgorithmException e) {
+			return null;
+		}
+	}
+	
+	public void save(Avatar avatar) {
+		String path = context.getFilesDir().getAbsolutePath() + "/avatars/";
+		File file = new File(path+"/"+avatar.getFilename());
+		file.getParentFile().mkdirs();
+		Log.d("xmppService",file.getAbsolutePath());
+		try {
+			file.createNewFile();
+			FileOutputStream mFileOutputStream = new FileOutputStream(file);
+			MessageDigest digest = MessageDigest.getInstance("SHA-1");
+			DigestOutputStream mDigestOutputStream = new DigestOutputStream(mFileOutputStream, digest);
+			mDigestOutputStream.write(avatar.getImageAsBytes());
+			mDigestOutputStream.flush();
+			mDigestOutputStream.close();
+			Log.d("xmppService","sha1sum after write: "+CryptoHelper.bytesToHex(digest.digest()));
+		} catch (FileNotFoundException e) {
+			
+		} catch (IOException e) {
+			Log.d("xmppService",e.getMessage());
+		} catch (NoSuchAlgorithmException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+	}
+
+	public Bitmap cropCenterSquare(Uri image, int size) {
+		try {
+			BitmapFactory.Options options = new BitmapFactory.Options();
+			options.inSampleSize = calcSampleSize(image, size);
+			InputStream is = context.getContentResolver()
+					.openInputStream(image);
+			Bitmap input = BitmapFactory.decodeStream(is, null, options);
+			int w = input.getWidth();
+			int h = input.getHeight();
+
+			float scale = Math.max((float) size / h, (float) size / w);
+
+			float outWidth = scale * w;
+			float outHeight = scale * h;
+			float left = (size - outWidth) / 2;
+			float top = (size - outHeight) / 2;
+			RectF target = new RectF(left, top, left + outWidth, top
+					+ outHeight);
+
+			Bitmap output = Bitmap.createBitmap(size, size, input.getConfig());
+			Canvas canvas = new Canvas(output);
+			canvas.drawBitmap(input, null, target, null);
+			return output;
+		} catch (FileNotFoundException e) {
+			return null;
+		}
+	}
+
+	private int calcSampleSize(Uri image, int size)
+			throws FileNotFoundException {
+		BitmapFactory.Options options = new BitmapFactory.Options();
+		options.inJustDecodeBounds = true;
+		BitmapFactory.decodeStream(context.getContentResolver()
+				.openInputStream(image), null, options);
+		int height = options.outHeight;
+		int width = options.outWidth;
+		int inSampleSize = 1;
+
+		if (height > size || width > size) {
+			int halfHeight = height / 2;
+			int halfWidth = width / 2;
+
+			while ((halfHeight / inSampleSize) > size
+					&& (halfWidth / inSampleSize) > size) {
+				inSampleSize *= 2;
+			}
+		}
+		return inSampleSize;
+
+	}
 
 	public class ImageCopyException extends Exception {
 		private static final long serialVersionUID = -1010013599132881427L;

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

@@ -53,6 +53,7 @@ import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
 import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.pep.Avatar;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
@@ -64,6 +65,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.database.ContentObserver;
+import android.graphics.Bitmap;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 import android.net.Uri;
@@ -1183,6 +1185,17 @@ public class XmppConnectionService extends Service {
 		}
 	}
 
+	
+	public void pushAvatar(Account account, Uri image) {
+		Avatar avatar = getFileBackend().getPepAvatar(image, 192, Bitmap.CompressFormat.WEBP);
+		if (avatar!=null) {
+			Log.d(LOGTAG,avatar.sha1sum);
+			Log.d(LOGTAG,avatar.image);
+			avatar.type = "image/webp";
+			getFileBackend().save(avatar);
+		}
+	}
+	
 	public void deleteContactOnServer(Contact contact) {
 		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
 		contact.resetOption(Contact.Options.DIRTY_PUSH);

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

@@ -99,6 +99,8 @@ public class ManageAccountActivity extends XmppActivity {
 				xmppConnectionService
 						.updateAccount(selectedAccountForActionMode);
 				mode.finish();
+			} else if (item.getItemId() == R.id.mgmt_account_publish_avatar) {
+				startActivity(new Intent(getApplicationContext(), PublishProfilePictureActivity.class));
 			} else if (item.getItemId() == R.id.mgmt_account_delete) {
 				AlertDialog.Builder builder = new AlertDialog.Builder(activity);
 				builder.setTitle(getString(R.string.mgmt_account_are_you_sure));

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

@@ -0,0 +1,101 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.utils.PhoneHelper;
+
+public class PublishProfilePictureActivity extends XmppActivity {
+	
+	private static final int REQUEST_CHOOSE_FILE = 0xac23;
+	
+	private ImageView avatar;
+	private TextView explanation;
+	private Button cancelButton;
+	private Button publishButton;
+	
+	private Uri avatarUri;
+	
+	@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.publishButton.setOnClickListener(new OnClickListener() {
+			
+			@Override
+			public void onClick(View v) {
+				if (avatarUri!=null) {
+					xmppConnectionService.pushAvatar(null, avatarUri);
+					finish();
+				}
+			}
+		});
+		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();
+				attachFileIntent.setType("image/*");
+				attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
+				Intent chooser = Intent.createChooser(attachFileIntent,
+						getString(R.string.attach_file));
+				startActivityForResult(chooser, REQUEST_CHOOSE_FILE);
+			}
+		});
+	}
+	
+	@Override
+	protected void onActivityResult(int requestCode, int resultCode,
+			final Intent data) {
+		super.onActivityResult(requestCode, resultCode, data);
+		Log.d("xmppService","on activity result");
+		if (resultCode == RESULT_OK) {
+			if (requestCode == REQUEST_CHOOSE_FILE) {
+				Log.d("xmppService","bla");
+				this.avatarUri = data.getData();
+			}
+		}
+	}
+
+	@Override
+	protected void onBackendConnected() {
+		Log.d("xmppService","on backend connected");
+		if (this.avatarUri == null) {
+			avatarUri = PhoneHelper.getSefliUri(getApplicationContext());
+		}
+		loadImageIntoPreview(avatarUri);
+		String explainText = getString(R.string.publish_avatar_explanation,"daniel@gultsch.de");
+		this.explanation.setText(explainText);
+	}
+	
+	protected void loadImageIntoPreview(Uri uri) {
+		Bitmap bm = xmppConnectionService.getFileBackend().cropCenterSquare(uri, 384);
+		this.avatar.setImageBitmap(bm);
+		enablePublishButton();
+	}
+	
+	protected void enablePublishButton() {
+		this.publishButton.setEnabled(true);
+		this.publishButton.setTextColor(getPrimaryTextColor());
+	}
+
+}

src/eu/siacs/conversations/utils/PhoneHelper.java 🔗

@@ -70,7 +70,7 @@ public class PhoneHelper {
 
 	public static Uri getSefliUri(Context context) {
 		String[] mProjection = new String[] { Profile._ID,
-				Profile.PHOTO_THUMBNAIL_URI };
+				Profile.PHOTO_URI };
 		Cursor mProfileCursor = context.getContentResolver().query(
 				Profile.CONTENT_URI, mProjection, null, null, null);
 

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

@@ -0,0 +1,23 @@
+package eu.siacs.conversations.xmpp.pep;
+
+import android.util.Base64;
+
+public class Avatar {
+	public String type;
+	public String sha1sum;
+	public String image;
+	public byte[] getImageAsBytes() {
+		return Base64.decode(image, Base64.DEFAULT);
+	}
+	public String getFilename() {
+		if (type==null) {
+			return sha1sum;
+		} else if (type.equalsIgnoreCase("image/webp")) {
+			return sha1sum+".webp";
+		} else if (type.equalsIgnoreCase("image/png")) {
+			return sha1sum+".png";
+		} else {
+			return sha1sum;
+		}
+	}
+}