/* Yaaic - Yet Another Android IRC Client Copyright 2009-2013 Sebastian Kaspari This file is part of Yaaic. Yaaic is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Yaaic is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Yaaic. If not, see <http://www.gnu.org/licenses/>. */ package indrora.atomic.model; import indrora.atomic.App; import indrora.atomic.utils.MircColors; import indrora.atomic.utils.Smilies; import java.util.Date; import java.util.Locale; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import android.text.style.ImageSpan; import android.widget.TextView; /** * A channel or server message * * @author Sebastian Kaspari <sebastian@yaaic.org> */ public class Message implements Parcelable { protected Message(Parcel in) { text = in.readString(); sender = in.readString(); timestamp = in.readLong(); type = in.readInt(); icon = in.readInt(); } public static final Creator<Message> CREATOR = new Creator<Message>() { @Override public Message createFromParcel(Parcel in) { return new Message(in); } @Override public Message[] newArray(int size) { return new Message[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(text); parcel.writeString(sender); parcel.writeLong(timestamp); parcel.writeInt(type); parcel.writeInt(icon); } public enum MessageColor { USER_EVENT, CHANNEL_EVENT, SERVER_EVENT, TOPIC, HIGHLIGHT, ERROR, DEFAULT, NO_COLOR } private static Settings settings; /* normal message, this is the default */ public static final int TYPE_MESSAGE = 0; public static final int TYPE_ACTION = 2; public static final int TYPE_SERVER = 3; /* join, part or quit */ public static final int TYPE_MISC = 1; public static final int NO_ICON = -1; public static final int NO_TYPE = -1; public static final int NO_COLOR = -1; private final String text; private final String sender; private long timestamp; private MessageColor color = MessageColor.DEFAULT; private int type = NO_TYPE; private int icon = NO_ICON; /** * Create a new message without an icon defaulting to TYPE_MESSAGE * * @param text */ public Message(String text) { this(text, null, TYPE_MESSAGE); } /** * Create a new message without an icon with a specific type * * @param text * @param type Message type */ public Message(String text, int type) { this(text, null, type); } /** * Create a new message sent by a user, without an icon, * defaulting to TYPE_MESSAGE * * @param text * @param sender */ public Message(String text, String sender) { this(text, sender, TYPE_MESSAGE); } /** * Create a new message sent by a user without an icon * * @param text * @param sender * @param type Message type */ public Message(String text, String sender, int type) { this(text, sender, type, new Date().getTime()); } public Message(String text, String sender, int type, long time) { this.text = text; this.sender = sender; this.timestamp = time; this.type = type; if( settings == null ) { settings = App.getSettings(); } } /** * Set the message's icon */ public void setIcon(int icon) { this.icon = icon; } /** * Get the message's icon * * @return */ public int getIcon() { return icon; } /** * Get the text of this message * * @return */ public String getText() { return text; } /** * Get the type of this message * * @return One of Message.TYPE_* */ public int getType() { return type; } public void setType(int t) { this.type = t; } /** * Set the color of this message */ public void setColor(MessageColor color) { this.color = color; } private static int translateColor(ColorScheme scheme, MessageColor c) { switch ( c ) { case CHANNEL_EVENT: return scheme.getChannelEvent(); case DEFAULT: return scheme.getForeground(); case ERROR: return scheme.getError(); case HIGHLIGHT: return scheme.getHighlight(); case SERVER_EVENT: return scheme.getServerEvent(); case TOPIC: return scheme.getTopic(); case USER_EVENT: return scheme.getUserEvent(); default: return scheme.getForeground(); } } /** * Set the timestamp of the message * * @param timestamp */ public void setTimestamp(long timestamp) { this.timestamp = timestamp; } private int getSenderColor(ColorScheme scheme) { return getSenderColor(this.sender, scheme); } /** * Associate a color with a sender name * * @return a color hexa */ public static int getSenderColor(String sender, ColorScheme scheme) { /* It might be worth to use some hash table here */ if( sender == null ) { return scheme.getForeground(); } if( !App.getSettings().showColorsNick() ) { return scheme.getForeground(); } int color = 0; int variant = sender.charAt(0); for( int i = 0; i < sender.length(); i++ ) { char c = sender.charAt(i); if( c - 33 > 'Z' ) variant += (c - 33) % 32; else variant -= (c - 33) % 32; color += c; } variant %= 20; // We don't want the color to be the background color. final int bg = scheme.getBackground(); int tmpColor;// = _scheme.getMircColor(color); do { float[] hsv = new float[3]; Color.colorToHSV(scheme.getMircColor(color++), hsv); hsv[0] += variant; tmpColor = Color.HSVToColor(hsv); } while ( likeness(bg, tmpColor) < 30 ); return tmpColor; //colors[color]; } /** * Calculates a likeness. This will return between 0-255 * on the likeness of the color. * * @param back * @param fore * @return */ private static int likeness(int back, int fore) { double gamma = 2.2; // Woo constants. double backL = 0.2126 * Math.pow((float)Color.red(back) / 255.0, gamma) + 0.7152 * Math.pow((float)Color.green(back) / 255.0, gamma) + 0.0722 * Math.pow((float)Color.blue(back) / 255.0, gamma); double foreL = 0.2126 * Math.pow((float)Color.red(fore) / 255.0, gamma) + 0.7152 * Math.pow((float)Color.green(fore) / 255.0, gamma) + 0.0722 * Math.pow((float)Color.blue(fore) / 255.0, gamma); int distance = (int)(255 * Math.abs(backL - foreL)); return distance; } private SpannableString _cache = null; MessageRenderParams currentParams = new MessageRenderParams(); Conversation _parent; protected void setConversation(Conversation p) { _parent = p; } public static SpannableString render(Message msg, MessageRenderParams renderParams) { ColorScheme renderedScheme = new ColorScheme(renderParams.colorScheme, renderParams.useDarkScheme); SpannableString nickSS; SpannableString timeSS; SpannableString messageSS; SpannableString prefixSS; // format the sender name if( msg.hasSender() ) { // The sender name spannable is just the name of the sender nickSS = new SpannableString(msg.sender); // Defaultly, the foreground color is used. int senderColor = renderedScheme.getForeground(); if( renderParams.nickColors ) { // getSenderColor does a variant color from the color scheme options. senderColor = getSenderColor(msg.sender, renderedScheme); // msg.getSenderColor(); } // We should now set the spannable's color. nickSS.setSpan(new ForegroundColorSpan(senderColor), 0, nickSS.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); // and wrap it in our preferred <'s if( msg.type == TYPE_MESSAGE ) { nickSS = new SpannableString(TextUtils.concat("<", nickSS, ">")); } } else { // There's no sender, so we give it no sender. nickSS = new SpannableString(""); } // Timestamps are handled in much the same way as the Sender, however they're much simpler. if( settings.showTimestamp() ) { timeSS = new SpannableString(msg.renderTimeStamp(renderParams.timestamp24Hour, renderParams.timestampSeconds)); } else { timeSS = new SpannableString(""); } // Prefix. // this is a space normally, however it becomes a *, then is filled with the appropriate // drawables. prefixSS = new SpannableString(" "); // If there is an icon, if( msg.hasIcon() ) { // We want to handle having an icon and not wanting to show icons. // this makes things a little nicer. prefixSS = new SpannableString("•"); // If we really want to show icons... if( renderParams.icons ) { Drawable drawable = App.getSResources().getDrawable(msg.icon); float density = App.getSResources().getDisplayMetrics().density; // scale = wanted / actual // This call is < x,y, width,height> // SpaceWidth is going to be in raw pixels, so we need to multiply it by density. // Height is going to be the drawable intrinsic height * scale * density // We need y to be 1/2 the height of the line, minus one half the height of the scaled drawable. Paint p = new Paint(); p.setTypeface(Typeface.MONOSPACE); p.setTextSize(settings.getFontSize()*density); int width = (int)(p.measureText(" ")); int fontheight = settings.getFontSize(); drawable.setBounds(0, (int)(-0.5*fontheight), (int)(width), (int)(width)); // And now we do the magic. prefixSS.setSpan(new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } // Now to the actual message. // How this works is that we're going to render it to a string, // if mIRC colors // => highlight color? // => blarg the spannable. // => blarg mIRC colors into the spannable. // else // => strip mIRC color tags from the text block // => highlight color? // => blarg the spannable. messageSS = new SpannableString(msg.text); if( renderParams.mircColors ) { if( msg.hasColor() && renderParams.messageColors ) { messageSS.setSpan(new ForegroundColorSpan(translateColor(renderedScheme, msg.color)), 0, messageSS.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } messageSS = MircColors.toSpannable(messageSS, renderedScheme); } else { messageSS = new SpannableString(MircColors.removeStyleAndColors(msg.text)); if( msg.hasColor() && renderParams.messageColors ) { messageSS.setSpan(new ForegroundColorSpan(translateColor(renderedScheme, msg.color)), 0, messageSS.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } // Smash smileys into this, but only if we're not a server message.. if( renderParams.smileys && msg.type != TYPE_SERVER ) { messageSS = Smilies.toSpannable(messageSS, App.getAppContext()); } return new SpannableString(TextUtils.concat(timeSS, prefixSS, nickSS, " ", messageSS)); } /** * Render message as spannable string * * @return */ public TextView render(TextView convertView) { // check if we just want to return ourselves already. if(!settings.getRenderParams().equals(currentParams)) { currentParams = settings.getRenderParams(); } Message t = (Message)convertView.getTag(); if(this.equals(t) && settings.getRenderParams().equals(t.currentParams)) { return convertView; } ColorScheme currentScheme = new ColorScheme(currentParams.colorScheme, currentParams.useDarkScheme); //_cache = new SpannableString(TextUtils.concat(timeSS, prefixSS, nickSS, " ", messageSS)); convertView.setTextColor(currentScheme.getForeground()); convertView.setLinkTextColor(currentScheme.getUrl()); convertView.setText(Message.render(this, currentParams)); convertView.setTag(this); currentParams = settings.getRenderParams(); return convertView; } /** * Does this message have a sender? * * @return */ public boolean hasSender() { return sender != null; } /** * Get the sender * @return the sender name */ public String getSender() { return sender; } /** * Does this message have a color assigned? * * @return */ private boolean hasColor() { return color != MessageColor.NO_COLOR; } /** * Does this message have an icon assigned? * * @return */ private boolean hasIcon() { return icon != NO_ICON; } /** * Generate a timestamp * * @param use24hFormat * @return */ public String renderTimeStamp(boolean use24hFormat, boolean includeSeconds) { Date date = new Date(timestamp); String format = "["; format += (use24hFormat ? "HH" : "hh"); format += ":mm"; if( includeSeconds ) { format += ":ss"; } format += "]"; return (String)(new java.text.SimpleDateFormat(format, Locale.US)).format(date); } @Override public boolean equals(Object o) { Message m = (Message)o; // If it's null, it's not us. if(m == null) return false; // if its icon is different, it's not us. if(this.hasIcon() != m.hasIcon()) return false; if(m.icon != this.icon) return false; // if the text doesn't match, it's not us. if(!this.text.equals(m.text)) return false; // if the sender isn't the same, it's not us. if(this.hasSender() != m.hasSender()) return false; if( this.sender != null && !this.sender.equals(m.sender) ) return false; // if the timestamp isn't the same, it's not us. if(this.timestamp != m.timestamp) return false; // if the type isn't the same, it's not us. if(this.type != m.type) return false; // if the message color isn't the same, it's not us. if(this.color != m.color) return false; // We're here, and that's fine. return true; } }