/*
* Kontalk Android client
* Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
* This program 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.
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kontalk.ui.view;
import java.util.regex.Pattern;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.afollestad.materialdialogs.simplelist.MaterialSimpleListAdapter;
import com.afollestad.materialdialogs.simplelist.MaterialSimpleListItem;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewStub;
import android.widget.Checkable;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.kontalk.R;
import org.kontalk.data.Contact;
import org.kontalk.message.CompositeMessage;
import org.kontalk.message.GroupComponent;
import org.kontalk.util.MessageUtils;
import org.kontalk.util.Preferences;
/**
* A message list item to be used in {@link org.kontalk.ui.ComposeMessage} activity.
* @author Daniele Ricci
* @version 1.0
*/
public class MessageListItem extends RelativeLayout implements Checkable {
private static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked };
private CompositeMessage mMessage;
// for message details
private String mPeer;
private String mDisplayName;
private MessageListItemTheme mBalloonTheme;
private TextView mDateHeader;
private boolean mChecked;
/*
private LeadingMarginSpan mLeadingMarginSpan;
private LineHeightSpan mSpan = new LineHeightSpan() {
public void chooseHeight(CharSequence text, int start,
int end, int spanstartv, int v, FontMetricsInt fm) {
fm.ascent -= 10;
}
};
private TextAppearanceSpan mTextSmallSpan =
new TextAppearanceSpan(getContext(), android.R.style.TextAppearance_Small);
*/
public MessageListItem(Context context) {
super(context);
}
public MessageListItem(final Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mDateHeader = (TextView) findViewById(R.id.date_header);
}
public void afterInflate(int direction, boolean event, boolean groupChat) {
ViewStub stub = (ViewStub) findViewById(R.id.balloon_stub);
String theme = groupChat ?
Preferences.getBalloonGroupsTheme(getContext()) :
Preferences.getBalloonTheme(getContext());
mBalloonTheme = MessageListItemThemeFactory.createTheme(theme, direction, event);
mBalloonTheme.inflate(stub);
}
public final void bind(Context context, final CompositeMessage msg, final Pattern highlight,
int itemType, int previousItemType, long previousTimestamp, String previousPeer,
Object... args) {
mMessage = msg;
setChecked(false);
boolean sameMessageBlock = false;
long msgTs = MessageUtils.getMessageTimestamp(mMessage);
boolean sameDate = MessageUtils.isSameDate(msgTs, previousTimestamp);
if (sameDate) {
mDateHeader.setVisibility(View.GONE);
// same day, check if it's also same direction and user
// some themes will use this information to group messages together
String msgPeer = MessageUtils.getMessagePeer(mMessage);
sameMessageBlock = (itemType == previousItemType && msgPeer.equals(previousPeer));
}
else {
mDateHeader.setText(MessageUtils.formatDateString(context, msgTs));
mDateHeader.setVisibility(View.VISIBLE);
}
mBalloonTheme.setSecurityFlags(mMessage.getSecurityFlags());
if (mMessage.getSender() != null) {
Contact contact = Contact.findByUserId(context, msg.getSender(true));
mBalloonTheme.setIncoming(contact, sameMessageBlock);
mPeer = contact.getNumber();
mDisplayName = contact.getDisplayName();
if (mPeer == null)
mPeer = msg.getSender(true);
}
else {
Contact contact = null;
if (!msg.hasComponent(GroupComponent.class))
contact = Contact.findByUserId(context, msg.getRecipients().get(0));
mBalloonTheme.setOutgoing(contact, mMessage.getStatus(), sameMessageBlock);
if (contact != null) {
mPeer = contact.getNumber();
mDisplayName = contact.getName();
}
if (mPeer == null)
mPeer = msg.getRecipients().get(0);
}
mBalloonTheme.setTimestamp(formatTimestamp());
if (msg.isEncrypted()) {
mBalloonTheme.setEncryptedContent(mMessage.getDatabaseId());
}
else {
// process components
Object[] argsAppend = null;
if (args != null) {
argsAppend = new Object[args.length+1];
System.arraycopy(args, 0, argsAppend, 0, args.length);
argsAppend[args.length] = mBalloonTheme;
}
mBalloonTheme.processComponents(mMessage.getDatabaseId(),
highlight, msg.getComponents(), argsAppend);
}
}
/*
private SpannableStringBuilder formatMessage(final Contact contact, final Pattern highlight) {
SpannableStringBuilder buf;
if (mMessage.isEncrypted()) {
buf = new SpannableStringBuilder(getResources().getString(R.string.text_encrypted));
}
else {
// this is used later to add \n at the end of the image placeholder
boolean thumbnailOnly;
TextComponent txt = (TextComponent) mMessage.getComponent(TextComponent.class);
String textContent = txt != null ? txt.getContent() : null;
if (TextUtils.isEmpty(textContent)) {
buf = new SpannableStringBuilder();
thumbnailOnly = true;
}
else {
buf = new SpannableStringBuilder(textContent);
thumbnailOnly = false;
}
// convert smileys first
int c = buf.length();
if (c > 0 && c < MAX_AFFORDABLE_SIZE)
MessageUtils.convertSmileys(getContext(), buf, SmileyImageSpan.SIZE_EDITABLE);
// image component: show image before text
AttachmentComponent attachment = (AttachmentComponent) mMessage
.getComponent(AttachmentComponent.class);
if (attachment != null) {
if (attachment instanceof ImageComponent) {
ImageComponent img = (ImageComponent) attachment;
// prepend some text for the ImageSpan
String placeholder = CompositeMessage.getSampleTextContent(img.getContent().getMime());
buf.insert(0, placeholder);
// add newline if there is some text after
if (!thumbnailOnly)
buf.insert(placeholder.length(), "\n");
Bitmap bitmap = img.getBitmap();
if (bitmap != null) {
ImageSpan imgSpan = new MaxSizeImageSpan(getContext(), bitmap);
buf.setSpan(imgSpan, 0, placeholder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
else {
// other component: show sample content if no body was found
if (txt == null)
buf.append(CompositeMessage.getSampleTextContent(attachment.getMime()));
}
}
}
if (highlight != null) {
Matcher m = highlight.matcher(buf.toString());
while (m.find())
buf.setSpan(mHighlightColorSpan, m.start(), m.end(), 0);
}
return buf;
}
*/
private CharSequence formatTimestamp() {
return MessageUtils.formatTimeString(getContext(), MessageUtils.getMessageTimestamp(mMessage));
}
public final void unbind() {
// TODO mMessage.recycle();
mMessage = null;
mBalloonTheme.unload();
}
@Override
public boolean isChecked() {
return mChecked;
}
@Override
public void setChecked(boolean checked) {
if (checked != mChecked) {
mChecked = checked;
refreshDrawableState();
}
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
@Override
public void toggle() {
setChecked(!mChecked);
}
// Thanks to Google Mms app :)
public void onClick() {
TextContentView textContent = mBalloonTheme.getTextContentView();
if (textContent == null)
return;
// Check for links. If none, do nothing; if 1, open it; if >1, ask user to pick one
final URLSpan[] spans = textContent.getUrls();
if (spans.length == 0) {
// show the message details dialog
MessageUtils.showMessageDetails(getContext(), mMessage, mPeer, mDisplayName);
}
else if (spans.length == 1) {
// show link opener
spans[0].onClick(textContent);
}
else {
// complex stuff (media)
URLSpanAdapterCallback click = new URLSpanAdapterCallback(textContent);
final MaterialSimpleListAdapter adapter = new MaterialSimpleListAdapter(click);
for (URLSpan span : spans) {
MaterialSimpleListItem.Builder builder = new MaterialSimpleListItem.Builder(getContext())
.tag(span);
try {
String url = span.getURL();
Uri uri = Uri.parse(url);
final String telPrefix = "tel:";
if (url.startsWith(telPrefix)) {
// TODO handle country code
url = url.substring(telPrefix.length());
}
builder.content(url);
Drawable d = getContext().getPackageManager().getActivityIcon(
new Intent(Intent.ACTION_VIEW, uri));
if (d != null) {
builder.icon(d).iconPadding(10);
}
}
catch (android.content.pm.PackageManager.NameNotFoundException ex) {
// it's ok if we're unable to set the drawable for this view - the user
// can still use it
}
adapter.add(builder.build());
}
new MaterialDialog.Builder(getContext())
.title(R.string.chooser_select_link)
.cancelable(true)
.adapter(adapter, null)
.negativeText(android.R.string.cancel)
.onNegative(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
dialog.dismiss();
}
})
.show();
}
}
public CompositeMessage getMessage() {
return mMessage;
}
private static final class URLSpanAdapterCallback implements MaterialSimpleListAdapter.Callback {
private TextView mParent;
URLSpanAdapterCallback(TextView parent) {
mParent = parent;
}
@Override
public void onMaterialListItemSelected(MaterialDialog dialog, int index, MaterialSimpleListItem item) {
if (item != null && item.getTag() != null)
((URLSpan) item.getTag()).onClick(mParent);
dialog.dismiss();
}
}
}