From 65aca2d8a7bbb52d380b53cde9fcf5a22590c303 Mon Sep 17 00:00:00 2001
From: Joshua Bahnsen <archrival@gmail.com>
Date: Wed, 29 Jan 2014 00:47:56 -0700
Subject: [PATCH] Add avatar support to the chat window, minor tweaks to the
 chat layout

---
 res/layout/chat.xml                           |   3 +-
 res/layout/chat_item.xml                      |  90 +++++++-----
 res/layout/chat_item_reverse.xml              | 100 +++++++------
 .../androidapp/domain/ChatMessage.java        |  20 +++
 .../service/CachedMusicService.java           |   6 +
 .../androidapp/service/MusicService.java      |   2 +
 .../service/OfflineMusicService.java          |  14 ++
 .../androidapp/service/RESTMusicService.java  |  82 +++++++++++
 .../ultrasonic/androidapp/util/FileUtil.java  |  83 +++++++++++
 .../androidapp/util/ImageLoader.java          | 132 +++++++++++++++++-
 .../ultrasonic/androidapp/util/Util.java      |  14 ++
 .../androidapp/view/ChatAdapter.java          |  40 ++++--
 12 files changed, 497 insertions(+), 89 deletions(-)

diff --git a/res/layout/chat.xml b/res/layout/chat.xml
index 0a04dfe8..8b8ea719 100644
--- a/res/layout/chat.xml
+++ b/res/layout/chat.xml
@@ -39,8 +39,9 @@
         
         <ImageButton
             a:id="@+id/chat_send"
-            a:layout_width="60dip"
+            a:layout_width="55dip"
             a:layout_height="40dip"
+            a:background="@color/transparent"
             a:src="?attr/chat_send" />
         
     </LinearLayout>
diff --git a/res/layout/chat_item.xml b/res/layout/chat_item.xml
index faaa2d64..3dad7870 100644
--- a/res/layout/chat_item.xml
+++ b/res/layout/chat_item.xml
@@ -1,48 +1,66 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
-    a:layout_width="fill_parent"
-    a:layout_height="wrap_content"
-    a:orientation="vertical" >
+<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
+                a:layout_width="fill_parent"
+                a:layout_height="wrap_content"
+                a:orientation="horizontal">
 
-    <TextView
-        a:id="@+id/chat_username"
-        a:layout_width="wrap_content"
-        a:layout_height="wrap_content"
-        a:layout_marginLeft="6dip"
-        a:layout_marginRight="6dip"
-        a:ellipsize="marquee"
-        a:singleLine="true"
-        a:text="User"
-        a:textAppearance="?android:attr/textAppearanceMedium"
-        a:textStyle="bold" />
+    <ImageView
+        a:layout_width="50dip"
+        a:layout_height="50dip"
+        a:id="@+id/chat_avatar"/>
 
     <LinearLayout
-        a:id="@+id/chat_message_layout"
-        a:layout_width="wrap_content"
+        a:layout_width="fill_parent"
         a:layout_height="wrap_content"
-        a:layout_marginTop="2dip"
-        a:orientation="horizontal" >
+        a:orientation="vertical"
+        a:layout_toEndOf="@id/chat_avatar"
+        a:gravity="center_vertical">
 
         <TextView
-            a:id="@+id/chat_time"
-            a:layout_width="wrap_content"
-            a:layout_height="wrap_content"
-            a:layout_marginLeft="6dip"
-            a:singleLine="true"
-            a:text="00:00"
-            a:textAppearance="?android:attr/textAppearanceMedium" />
-
-        <TextView
-            a:id="@+id/chat_message"
+            a:id="@+id/chat_username"
             a:layout_width="wrap_content"
             a:layout_height="wrap_content"
             a:layout_marginLeft="6dip"
             a:layout_marginRight="6dip"
-            a:autoLink="all"
-            a:linksClickable="true"
-            a:singleLine="false"
-            a:text="Message Text Goes Here"
-            a:textAppearance="?android:attr/textAppearanceMedium" />
-    </LinearLayout>
+            a:ellipsize="marquee"
+            a:singleLine="true"
+            a:textIsSelectable="true"
+            a:textAppearance="?android:attr/textAppearanceMedium"
+            a:textStyle="bold"
+            a:layout_gravity="left"
+            a:gravity="center_vertical|left"/>
 
-</LinearLayout>
\ No newline at end of file
+        <LinearLayout
+            a:id="@+id/chat_message_layout"
+            a:layout_width="wrap_content"
+            a:layout_height="wrap_content"
+            a:layout_marginTop="2dip"
+            a:orientation="horizontal"
+            a:gravity="center_vertical|left"
+            a:layout_gravity="left">
+
+            <TextView
+                a:id="@+id/chat_time"
+                a:layout_width="wrap_content"
+                a:layout_height="wrap_content"
+                a:layout_marginLeft="6dip"
+                a:singleLine="true"
+                a:textIsSelectable="true"
+                a:textAppearance="?android:attr/textAppearanceMedium"
+                a:gravity="left"/>
+
+            <TextView
+                a:id="@+id/chat_message"
+                a:layout_width="wrap_content"
+                a:layout_height="wrap_content"
+                a:layout_marginLeft="6dip"
+                a:layout_marginRight="6dip"
+                a:textIsSelectable="true"
+                a:linksClickable="true"
+                a:singleLine="false"
+                a:autoLink="all"
+                a:textAppearance="?android:attr/textAppearanceMedium"
+                a:gravity="left"/>
+        </LinearLayout>
+    </LinearLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/res/layout/chat_item_reverse.xml b/res/layout/chat_item_reverse.xml
index c6206286..39ca0b0f 100644
--- a/res/layout/chat_item_reverse.xml
+++ b/res/layout/chat_item_reverse.xml
@@ -1,51 +1,71 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
-    a:layout_width="fill_parent"
-    a:layout_height="wrap_content"
-    a:orientation="vertical" >
-
-    <TextView
-        a:id="@+id/chat_username"
-        a:layout_width="wrap_content"
-        a:layout_height="wrap_content"
-        a:layout_marginRight="6dip"
-        a:gravity="right"
-        a:layout_gravity="right"
-        a:ellipsize="marquee"
-        a:singleLine="true"
-        a:text="User"
-        a:textAppearance="?android:attr/textAppearanceMedium"
-        a:textStyle="bold" />
+<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
+              a:layout_width="fill_parent"
+              a:layout_height="wrap_content"
+              a:orientation="horizontal">
 
     <LinearLayout
-        a:layout_width="wrap_content"
+        a:layout_width="fill_parent"
         a:layout_height="wrap_content"
-        a:layout_marginTop="2dip"
-        a:orientation="horizontal"
-        a:layout_gravity="right" >
+        a:orientation="vertical"
+        a:layout_gravity="right"
+        a:layout_alignParentEnd="false"
+        a:layout_toStartOf="@id/chat_avatar"
+        a:gravity="center_vertical|right">
 
         <TextView
-            a:id="@+id/chat_time"
+            a:id="@+id/chat_username"
             a:layout_width="wrap_content"
             a:layout_height="wrap_content"
-            a:layout_marginLeft="6dip"
-            a:singleLine="true"
-            a:gravity="right"
-            a:text="00:00"
-            a:textAppearance="?android:attr/textAppearanceMedium" />
-
-        <TextView
-            a:id="@+id/chat_message"
-            a:layout_width="wrap_content"
-            a:layout_height="wrap_content"
-            a:layout_marginLeft="6dip"
             a:layout_marginRight="6dip"
-            a:autoLink="all"
-            a:linksClickable="true"
-            a:singleLine="false"
-            a:gravity="right"
-            a:text="Chat message"
-            a:textAppearance="?android:attr/textAppearanceMedium" />
+            a:gravity="center_vertical|right"
+            a:layout_gravity="right"
+            a:ellipsize="marquee"
+            a:singleLine="true"
+            a:textIsSelectable="true"
+            a:textAppearance="?android:attr/textAppearanceMedium"
+            a:textStyle="bold"/>
+
+        <LinearLayout
+            a:layout_width="wrap_content"
+            a:layout_height="wrap_content"
+            a:layout_marginTop="2dip"
+            a:orientation="horizontal"
+            a:layout_gravity="right|end"
+            a:gravity="center_vertical|right">
+
+            <TextView
+                a:id="@+id/chat_time"
+                a:layout_width="wrap_content"
+                a:layout_height="wrap_content"
+                a:layout_marginLeft="6dip"
+                a:singleLine="true"
+                a:gravity="center_vertical|right"
+                a:textIsSelectable="true"
+                a:textAppearance="?android:attr/textAppearanceMedium"
+                a:layout_gravity="right"/>
+
+            <TextView
+                a:id="@+id/chat_message"
+                a:layout_width="wrap_content"
+                a:layout_height="wrap_content"
+                a:layout_marginLeft="6dip"
+                a:layout_marginRight="6dip"
+                a:linksClickable="true"
+                a:singleLine="false"
+                a:autoLink="all"
+                a:textIsSelectable="true"
+                a:textAppearance="?android:attr/textAppearanceMedium"
+                a:gravity="center_vertical|right"
+                a:layout_gravity="right"/>
+        </LinearLayout>
     </LinearLayout>
 
-</LinearLayout>
\ No newline at end of file
+    <ImageView
+        a:layout_width="50dip"
+        a:layout_height="50dip"
+        a:id="@+id/chat_avatar"
+        a:layout_alignParentEnd="true"
+        a:layout_toStartOf="@id/chat_avatar"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/domain/ChatMessage.java b/src/com/thejoshwa/ultrasonic/androidapp/domain/ChatMessage.java
index e5b0adfc..258e2119 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/domain/ChatMessage.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/domain/ChatMessage.java
@@ -41,4 +41,24 @@ public class ChatMessage implements Serializable
 	{
 		this.message = message;
 	}
+
+	@Override
+	public boolean equals(Object o)
+	{
+		if (this == o) return true;
+		if (o == null || getClass() != o.getClass()) return false;
+
+		ChatMessage that = (ChatMessage) o;
+
+		return message.equals(that.message) && time.equals(that.time) && username.equals(that.username);
+	}
+
+	@Override
+	public int hashCode()
+	{
+		int result = username.hashCode();
+		result = 31 * result + time.hashCode();
+		result = 31 * result + message.hashCode();
+		return result;
+	}
 }
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/service/CachedMusicService.java b/src/com/thejoshwa/ultrasonic/androidapp/service/CachedMusicService.java
index f7884562..1a700357 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/service/CachedMusicService.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/service/CachedMusicService.java
@@ -526,4 +526,10 @@ public class CachedMusicService implements MusicService
 	{
 		musicService.updateShare(id, description, expires, context, progressListener);
 	}
+
+	@Override
+	public Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception
+	{
+		return musicService.getAvatar(context, username, size, saveToFile, highQuality, progressListener);
+	}
 }
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/service/MusicService.java b/src/com/thejoshwa/ultrasonic/androidapp/service/MusicService.java
index f963e8cb..a082a855 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/service/MusicService.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/service/MusicService.java
@@ -147,4 +147,6 @@ public interface MusicService
 	void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception;
 
 	void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception;
+
+	Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception;
 }
\ No newline at end of file
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/service/OfflineMusicService.java b/src/com/thejoshwa/ultrasonic/androidapp/service/OfflineMusicService.java
index 24de3c4d..1c89e2bf 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/service/OfflineMusicService.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/service/OfflineMusicService.java
@@ -328,6 +328,20 @@ public class OfflineMusicService extends RESTMusicService
 		return entry;
 	}
 
+	@Override
+	public Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception
+	{
+		try
+		{
+			Bitmap bitmap = FileUtil.getAvatarBitmap(username, size, highQuality);
+			return Util.scaleBitmap(bitmap, size);
+		}
+		catch (Exception e)
+		{
+			return null;
+		}
+	}
+
 	@Override
 	public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception
 	{
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/service/RESTMusicService.java b/src/com/thejoshwa/ultrasonic/androidapp/service/RESTMusicService.java
index cb08e549..11935dc3 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/service/RESTMusicService.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/service/RESTMusicService.java
@@ -872,6 +872,13 @@ public class RESTMusicService implements MusicService
 		}
 	}
 
+	private static boolean checkServerVersion(Context context, String version)
+	{
+		Version serverVersion = Util.getServerRestVersion(context);
+		Version requiredVersion = new Version(version);
+		return serverVersion == null || serverVersion.compareTo(requiredVersion) >= 0;
+	}
+
 	@Override
 	public Bitmap getCoverArt(Context context, final MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception
 	{
@@ -1659,4 +1666,79 @@ public class RESTMusicService implements MusicService
 			Util.close(reader);
 		}
 	}
+
+	@Override
+	public Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception
+	{
+		// Return silently if server is too old
+		if (!checkServerVersion(context, "1.8"))
+			return null;
+
+		// Synchronize on the username so that we don't download concurrently for
+		// the same user.
+		if (username == null)
+		{
+			return null;
+		}
+
+		synchronized (username)
+		{
+			// Use cached file, if existing.
+			Bitmap bitmap = FileUtil.getAvatarBitmap(username, size, highQuality);
+
+			if (bitmap == null)
+			{
+				String url = Util.getRestUrl(context, "getAvatar");
+
+				InputStream in = null;
+
+				try
+				{
+					List<String> parameterNames;
+					List<Object> parameterValues;
+
+					parameterNames = Collections.singletonList("username");
+					parameterValues = Arrays.<Object>asList(username);
+
+					HttpEntity entity = getEntityForURL(context, url, null, parameterNames, parameterValues, progressListener);
+					in = entity.getContent();
+
+					// If content type is XML, an error occurred. Get it.
+					String contentType = Util.getContentType(entity);
+					if (contentType != null && contentType.startsWith("text/xml"))
+					{
+						new ErrorParser(context).parse(new InputStreamReader(in, Constants.UTF_8));
+						return null; // Never reached.
+					}
+
+					byte[] bytes = Util.toByteArray(in);
+
+					// If we aren't allowing server-side scaling, always save the file to disk because it will be unmodified
+					if (saveToFile)
+					{
+						OutputStream out = null;
+
+						try
+						{
+							out = new FileOutputStream(FileUtil.getAvatarFile(username));
+							out.write(bytes);
+						}
+						finally
+						{
+							Util.close(out);
+						}
+					}
+
+					bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality);
+				}
+				finally
+				{
+					Util.close(in);
+				}
+			}
+
+			// Return scaled bitmap
+			return Util.scaleBitmap(bitmap, size);
+		}
+	}
 }
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/util/FileUtil.java b/src/com/thejoshwa/ultrasonic/androidapp/util/FileUtil.java
index d4d9c159..1e8f9b03 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/util/FileUtil.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/util/FileUtil.java
@@ -111,6 +111,19 @@ public class FileUtil
 		return getAlbumArtFile(albumDir);
 	}
 
+	public static File getAvatarFile(String username)
+	{
+		File albumArtDir = getAlbumArtDirectory();
+
+		if (albumArtDir == null || username == null)
+		{
+			return null;
+		}
+
+		String md5Hex = Util.md5Hex(username);
+		return new File(albumArtDir, String.format("%s.jpeg", md5Hex));
+	}
+
 	public static File getAlbumArtFile(File albumDir)
 	{
 		File albumArtDir = getAlbumArtDirectory();
@@ -124,6 +137,76 @@ public class FileUtil
 		return new File(albumArtDir, String.format("%s.jpeg", md5Hex));
 	}
 
+	public static Bitmap getAvatarBitmap(String username, int size, boolean highQuality)
+	{
+		if (username == null) return null;
+
+		File avatarFile = getAvatarFile(username);
+
+		SubsonicTabActivity subsonicTabActivity = SubsonicTabActivity.getInstance();
+		Bitmap bitmap = null;
+		ImageLoader imageLoader = null;
+
+		if (subsonicTabActivity != null)
+		{
+			imageLoader = subsonicTabActivity.getImageLoader();
+
+			if (imageLoader != null)
+			{
+				bitmap = imageLoader.getImageBitmap(username, size);
+			}
+		}
+
+		if (bitmap != null)
+		{
+			return bitmap.copy(bitmap.getConfig(), false);
+		}
+
+		if (avatarFile != null && avatarFile.exists())
+		{
+			final BitmapFactory.Options opt = new BitmapFactory.Options();
+
+			if (size > 0)
+			{
+				opt.inJustDecodeBounds = true;
+				BitmapFactory.decodeFile(avatarFile.getPath(), opt);
+
+				if (highQuality)
+				{
+					opt.inDither = true;
+					opt.inPreferQualityOverSpeed = true;
+				}
+
+				opt.inPurgeable = true;
+				opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
+				opt.inJustDecodeBounds = false;
+			}
+
+			try
+			{
+				bitmap = BitmapFactory.decodeFile(avatarFile.getPath(), opt);
+			}
+			catch (Exception ex)
+			{
+				Log.e(TAG, "Exception in BitmapFactory.decodeFile()", ex);
+			}
+
+			Log.i("getAvatarBitmap", String.valueOf(size));
+
+			if (bitmap != null)
+			{
+				if (imageLoader != null)
+				{
+					imageLoader.addImageToCache(bitmap, username, size);
+				}
+			}
+
+			return bitmap == null ? null : bitmap;
+		}
+
+		return null;
+	}
+
 	public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size, boolean highQuality)
 	{
 		if (entry == null) return null;
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/util/ImageLoader.java b/src/com/thejoshwa/ultrasonic/androidapp/util/ImageLoader.java
index 961b78a2..1af2aa1e 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/util/ImageLoader.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/util/ImageLoader.java
@@ -58,6 +58,7 @@ public class ImageLoader implements Runnable
 	private int imageSizeDefault;
 	private final int imageSizeLarge;
 	private Bitmap largeUnknownImage;
+	private Bitmap unknownAvatarImage;
 	private Context context;
 	private Collection<Thread> threads;
 	private AtomicBoolean running = new AtomicBoolean();
@@ -80,6 +81,7 @@ public class ImageLoader implements Runnable
 
 		imageSizeLarge = Util.getMaxDisplayMetric(context);
 		createLargeUnknownImage(context);
+		createUnknownAvatarImage(context);
 	}
 
 	public synchronized boolean isRunning()
@@ -130,6 +132,41 @@ public class ImageLoader implements Runnable
 		}
 	}
 
+	private void createUnknownAvatarImage(Context context)
+	{
+		Resources res = context.getResources();
+		Drawable contact = res.getDrawable(R.drawable.ic_contact_picture);
+		unknownAvatarImage = Util.createBitmapFromDrawable(contact);
+	}
+
+	public void loadAvatarImage(View view, String username, boolean large, int size, boolean crossFade, boolean highQuality)
+	{
+		view.invalidate();
+
+		if (username == null)
+		{
+			setUnknownAvatarImage(view);
+			return;
+		}
+
+		if (size <= 0)
+		{
+			size = large ? imageSizeLarge : imageSizeDefault;
+		}
+
+		Bitmap bitmap = cache.get(getKey(username, size));
+
+		if (bitmap != null)
+		{
+			setAvatarImageBitmap(view, username, bitmap, crossFade);
+			return;
+		}
+
+		setUnknownAvatarImage(view);
+
+		queue.offer(new Task(view, username, size, large, crossFade, highQuality));
+	}
+
 	public void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, boolean crossFade, boolean highQuality)
 	{
 		view.invalidate();
@@ -171,6 +208,19 @@ public class ImageLoader implements Runnable
 		return String.format("%s:%d", coverArtId, size);
 	}
 
+	public Bitmap getImageBitmap(String username, int size)
+	{
+		Bitmap bitmap = cache.get(getKey(username, size));
+
+		if (bitmap != null && !bitmap.isRecycled())
+		{
+			Bitmap.Config config = bitmap.getConfig();
+			return bitmap.copy(config, false);
+		}
+
+		return null;
+	}
+
 	public Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size)
 	{
 		if (entry == null)
@@ -224,7 +274,7 @@ public class ImageLoader implements Runnable
 				if (existingDrawable == null)
 				{
 					Bitmap emptyImage = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
-					existingDrawable = new BitmapDrawable(emptyImage);
+					existingDrawable = new BitmapDrawable(context.getResources(), emptyImage);
 				}
 
 				Drawable[] layers = new Drawable[]{existingDrawable, newDrawable};
@@ -240,6 +290,50 @@ public class ImageLoader implements Runnable
 		}
 	}
 
+	private void setAvatarImageBitmap(View view, String username, Bitmap bitmap, boolean crossFade)
+	{
+		if (view instanceof ImageView)
+		{
+			ImageView imageView = (ImageView) view;
+
+			String tagEntry = (String) view.getTag();
+
+			// Only apply image to the view if the view is intended for this entry
+			if (username != null && tagEntry != null && !username.equals(tagEntry))
+			{
+				Log.i(TAG, "View is no longer valid, not setting ImageBitmap");
+				return;
+			}
+
+			if (crossFade)
+			{
+				Drawable existingDrawable = imageView.getDrawable();
+				Drawable newDrawable = Util.createDrawableFromBitmap(this.context, bitmap);
+
+				if (existingDrawable == null)
+				{
+					Bitmap emptyImage = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
+					existingDrawable = new BitmapDrawable(context.getResources(), emptyImage);
+				}
+
+				Drawable[] layers = new Drawable[]{existingDrawable, newDrawable};
+
+				TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
+				imageView.setImageDrawable(transitionDrawable);
+				transitionDrawable.startTransition(250);
+			}
+			else
+			{
+				imageView.setImageBitmap(bitmap);
+			}
+		}
+	}
+
+	public void setUnknownAvatarImage(View view)
+	{
+		setAvatarImageBitmap(view, null, unknownAvatarImage, false);
+	}
+
 	public void setUnknownImage(View view, boolean large)
 	{
 		if (large)
@@ -264,6 +358,11 @@ public class ImageLoader implements Runnable
 		cache.put(getKey(entry.getCoverArt(), size), bitmap);
 	}
 
+	public void addImageToCache(Bitmap bitmap, String username, int size)
+	{
+		cache.put(getKey(username, size), bitmap);
+	}
+
 	public void clear()
 	{
 		queue.clear();
@@ -295,6 +394,7 @@ public class ImageLoader implements Runnable
 	{
 		private final View view;
 		private final MusicDirectory.Entry entry;
+		private final String username;
 		private final Handler handler;
 		private final int size;
 		private final boolean saveToFile;
@@ -305,6 +405,19 @@ public class ImageLoader implements Runnable
 		{
 			this.view = view;
 			this.entry = entry;
+			this.username = null;
+			this.size = size;
+			this.saveToFile = saveToFile;
+			this.crossFade = crossFade;
+			this.highQuality = highQuality;
+			handler = new Handler();
+		}
+
+		public Task(View view, String username, int size, boolean saveToFile, boolean crossFade, boolean highQuality)
+		{
+			this.view = view;
+			this.entry = null;
+			this.username = username;
 			this.size = size;
 			this.saveToFile = saveToFile;
 			this.crossFade = crossFade;
@@ -317,16 +430,27 @@ public class ImageLoader implements Runnable
 			try
 			{
 				MusicService musicService = MusicServiceFactory.getMusicService(view.getContext());
-				final Bitmap bitmap = musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null);
+				final boolean isAvatar = this.username != null && this.entry == null;
+				final Bitmap bitmap = this.entry != null ? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) : musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null);
 
-				addImageToCache(bitmap, entry, size);
+				if (isAvatar)
+					addImageToCache(bitmap, username, size);
+				else
+					addImageToCache(bitmap, entry, size);
 
 				handler.post(new Runnable()
 				{
 					@Override
 					public void run()
 					{
-						setImageBitmap(view, entry, bitmap, crossFade);
+						if (isAvatar)
+						{
+							setAvatarImageBitmap(view, username, bitmap, crossFade);
+						}
+						else
+						{
+							setImageBitmap(view, entry, bitmap, crossFade);
+						}
 					}
 				});
 			}
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/util/Util.java b/src/com/thejoshwa/ultrasonic/androidapp/util/Util.java
index e3216912..f1cb024b 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/util/Util.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/util/Util.java
@@ -31,6 +31,7 @@ import android.content.pm.PackageManager;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.media.AudioManager;
@@ -871,6 +872,19 @@ public class Util extends DownloadActivity
 		return new BitmapDrawable(context.getResources(), bitmap);
 	}
 
+	public static Bitmap createBitmapFromDrawable(Drawable drawable) {
+		if (drawable instanceof BitmapDrawable) {
+			return ((BitmapDrawable)drawable).getBitmap();
+		}
+
+		Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+		Canvas canvas = new Canvas(bitmap);
+		drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+		drawable.draw(canvas);
+
+		return bitmap;
+	}
+
 	public static WifiManager.WifiLock createWifiLock(Context context, String tag)
 	{
 		WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
diff --git a/src/com/thejoshwa/ultrasonic/androidapp/view/ChatAdapter.java b/src/com/thejoshwa/ultrasonic/androidapp/view/ChatAdapter.java
index 14a824e8..5d81f706 100644
--- a/src/com/thejoshwa/ultrasonic/androidapp/view/ChatAdapter.java
+++ b/src/com/thejoshwa/ultrasonic/androidapp/view/ChatAdapter.java
@@ -6,34 +6,45 @@ import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
+import android.widget.ImageView;
 import android.widget.TextView;
 
 import com.thejoshwa.ultrasonic.androidapp.R;
 import com.thejoshwa.ultrasonic.androidapp.activity.SubsonicTabActivity;
 import com.thejoshwa.ultrasonic.androidapp.domain.ChatMessage;
+import com.thejoshwa.ultrasonic.androidapp.util.ImageLoader;
 import com.thejoshwa.ultrasonic.androidapp.util.Util;
 
 import java.text.DateFormat;
-import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 import java.util.regex.Pattern;
 
 public class ChatAdapter extends ArrayAdapter<ChatMessage>
 {
-
 	private final SubsonicTabActivity activity;
-	private ArrayList<ChatMessage> messages;
+	private List<ChatMessage> messages;
 
-	private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})"; //you can just place your support phone here
+	private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})";
 	private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
 
-	public ChatAdapter(SubsonicTabActivity activity, ArrayList<ChatMessage> messages)
+	public ChatAdapter(SubsonicTabActivity activity, List<ChatMessage> messages)
 	{
 		super(activity, R.layout.chat_item, messages);
 		this.activity = activity;
 		this.messages = messages;
 	}
 
+	@Override
+	public boolean areAllItemsEnabled() {
+		return true;
+	}
+
+	@Override
+	public boolean isEnabled(int position) {
+		return false;
+	}
+
 	@Override
 	public int getCount()
 	{
@@ -65,16 +76,25 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
 		{
 			holder = (ViewHolder) convertView.getTag();
 
-			if (holder.layout != layout)
+			if (!holder.chatMessage.equals(message))
 			{
 				convertView = inflateView(layout, parent);
 				holder = createViewHolder(layout, convertView);
 			}
 		}
 
+		holder.chatMessage = message;
+
 		DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(activity);
 		String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
 
+		ImageLoader imageLoader = activity.getImageLoader();
+
+		if (imageLoader != null)
+		{
+			imageLoader.loadAvatarImage(holder.avatar, messageUser, false, holder.avatar.getWidth(), false, true);
+		}
+
 		holder.username.setText(messageUser);
 		holder.message.setText(messageText);
 		holder.time.setText(messageTimeFormatted);
@@ -95,18 +115,20 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
 		TextView usernameView;
 		TextView timeView;
 		TextView messageView;
+		ImageView imageView;
 
 		if (convertView != null)
 		{
 			usernameView = (TextView) convertView.findViewById(R.id.chat_username);
 			timeView = (TextView) convertView.findViewById(R.id.chat_time);
 			messageView = (TextView) convertView.findViewById(R.id.chat_message);
+			imageView = (ImageView) convertView.findViewById(R.id.chat_avatar);
 
 			messageView.setMovementMethod(LinkMovementMethod.getInstance());
-			Linkify.addLinks(messageView, Linkify.EMAIL_ADDRESSES);
-			Linkify.addLinks(messageView, Linkify.WEB_URLS);
+			Linkify.addLinks(messageView, Linkify.ALL);
 			Linkify.addLinks(messageView, phoneMatcher, "tel:");
 
+			holder.avatar = imageView;
 			holder.message = messageView;
 			holder.username = usernameView;
 			holder.time = timeView;
@@ -120,8 +142,10 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
 	private static class ViewHolder
 	{
 		int layout;
+		ImageView avatar;
 		TextView message;
 		TextView username;
 		TextView time;
+		ChatMessage chatMessage;
 	}
 }