Add avatar support to the chat window, minor tweaks to the chat layout

This commit is contained in:
Joshua Bahnsen 2014-01-29 00:47:56 -07:00
parent d09c9499e6
commit 65aca2d8a7
12 changed files with 497 additions and 89 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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