org.retroshare.android.ConversationFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.retroshare.android.ConversationFragment.java

Source

/**
 * @license
 *
 * Copyright (c) 2013 Gioacchino Mazzurco <gio@eigenlab.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.retroshare.android;

import android.app.Activity;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.text.Html;
import android.text.Spanned;
import android.text.util.Linkify;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.EditText;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;

import org.apache.commons.lang.StringEscapeUtils;
import org.retroshare.android.utils.ColorHash;
import org.retroshare.android.utils.HtmlBase64ImageGetter;
import org.retroshare.android.utils.Util;

import java.io.FileNotFoundException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import org.retroshare.android.RsConversationService.ConversationId;
import org.retroshare.android.RsConversationService.ConversationMessage;
import org.retroshare.android.RsConversationService.ConversationEvent;
import org.retroshare.android.RsConversationService.ConversationEventKind;
import org.retroshare.android.RsConversationService.NewMessageConversationEvent;

public class ConversationFragment extends RsServiceClientFragmentBase
        implements View.OnKeyListener, ListView.OnScrollListener {
    @Override
    public String TAG() {
        return "ConversationFragment";
    }

    public static final String CONVERSATION_ID_EXTRA_KEY = "org.retroshare.android.extra_keys.conversationID";
    public static final int CONVERSATION_IMAGE_MAX_DIMENSION = 200;

    private ConversationId conversationId;

    private List<_ConversationMessage> messageList = new ArrayList<_ConversationMessage>();
    private final ConversationAdapter adapter = new ConversationAdapter();
    private ListView conversationMessageListView;
    private LayoutInflater mInflater;
    private View moreMessageUpIndicator;

    private HtmlBase64ImageGetter chatImageGetter;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View fv = inflater.inflate(R.layout.conversation_fragment, container, false);

        /**
         * Message List
         */
        conversationMessageListView = (ListView) fv.findViewById(R.id.chatMessageList);
        conversationMessageListView.setAdapter(adapter);

        /**
         * New message editor
         */
        View inputTextView = fv.findViewById(R.id.chatFragmentMessageEditText);
        inputTextView.setOnKeyListener(this);
        inputTextView.setOnLongClickListener(new OnShowSendExtraLongClickListener());

        /**
         * More items down indicator
         */
        View moreMessageDownIndicator = fv.findViewById(R.id.moreChatMessageDownImageView);
        moreMessageDownIndicator.setVisibility(View.INVISIBLE);
        moreMessageDownIndicator.setOnClickListener(new GoDownButtonListener());
        conversationMessageListView.setScrollIndicators(null, moreMessageDownIndicator);

        /**
         * More items up indicator ( Logic is different from down one )
         */
        moreMessageUpIndicator = fv.findViewById(R.id.moreChatMessageUpImageView);
        moreMessageUpIndicator.setVisibility(View.INVISIBLE);
        moreMessageUpIndicator.setOnClickListener(new GoUpButtonListener());
        conversationMessageListView.setOnScrollListener(this);

        /**
         * Send Extra menu
         */
        sendExtraMenu = fv.findViewById(R.id.sendExtraLayout);
        sendExtraMenu.setVisibility(View.INVISIBLE);
        sendExtraMenu.findViewById(R.id.sendImageButton).setOnClickListener(new OnAddImageClickListener());
        //      sendExtraMenu.findViewById(R.id.sendFileButton).setVisibility(View.GONE);
        sendExtraMenu.findViewById(R.id.showInfoImageButton)
                .setOnClickListener(new ShowConversationInfoLongClickListener());

        return fv;
    }

    @Override
    public void onAttach(Activity a) {
        super.onAttach(a);

        final Bundle arguments = getArguments();
        if (arguments.containsKey(CONVERSATION_ID_EXTRA_KEY))
            conversationId = arguments.getParcelable(CONVERSATION_ID_EXTRA_KEY);
        else
            throw new RuntimeException(TAG() + " need arguments contains valid value for "
                    + ConversationFragment.CONVERSATION_ID_EXTRA_KEY);

        chatImageGetter = new HtmlBase64ImageGetter(getResources());
        mInflater = a.getLayoutInflater();
    }

    @Override
    public void onResume() {
        super.onResume();
        initializeConversation();
    }

    @Override
    public void onPause() {
        if (isBound())
            getConnectedServer().mRsConversationService.enableNotificationForConversation(conversationId);
        super.onPause();
    }

    @Override
    public void onServiceConnected(ComponentName className, IBinder service) {
        super.onServiceConnected(className, service);
        initializeConversation();
    }

    @Override
    public void registerRsServicesListeners() {
        super.registerRsServicesListeners();
        getConnectedServer().mRsConversationService.registerRsConversationServiceListener(adapter);
    }

    @Override
    public void unregisterRsServicesListeners() {
        getConnectedServer().mRsConversationService.unregisterRsConversationServiceListener(adapter);
        super.unregisterRsServicesListeners();
    }

    private class ConversationAdapter implements ListAdapter, RsConversationService.RsConversationServiceListener {

        private class ReloadMessagesHistoryAsyncTask
                extends AsyncTask<Integer, Void, ReloadMessagesHistoryAsyncTask.MsgsBund> {
            @Override
            protected MsgsBund doInBackground(Integer... lastMineIntegers) {
                List<_ConversationMessage> fmsg = new ArrayList<_ConversationMessage>();
                int lastMineIndex = lastMineIntegers[0];

                if (isBound())
                    try {
                        List<ConversationMessage> msgs = (List<ConversationMessage>) getConnectedServer().mRsConversationService
                                .getConversationHistory(conversationId);
                        for (int i = 0; i < msgs.size(); ++i) {
                            _ConversationMessage msg = new _ConversationMessage(msgs.get(i));
                            fmsg.add(msg);
                            if ((i > lastMineIndex) && msg.isMine())
                                lastMineIndex = i;
                        }
                    } catch (RuntimeException e) {
                    }

                return new MsgsBund(fmsg, lastMineIndex);
            }

            @Override
            protected void onPostExecute(MsgsBund mb) {
                messageList = mb.messagesList;
                lastMineMessageIndex = mb.lastMinePosition;
                int lastPosition = messageList.size() - 1;
                conversationMessageListView.setSelection(lastPosition);
                adapter.notifyObservers();
                autoScrollTo(lastPosition, false);
            }

            public final class MsgsBund {
                public MsgsBund(List<_ConversationMessage> msgs, int lastMineIndex) {
                    messagesList = msgs;
                    lastMinePosition = lastMineIndex;
                };

                public final List<_ConversationMessage> messagesList;
                public final int lastMinePosition;
            }
        }

        /** Implements RsConversationServiceListener */
        private final Long handle = RsConversationService.RsConversationServiceListenerUniqueHandleFactory
                .getNewUniqueHandle();

        public Long getUniqueRsConversationServiceListenerHandle() {
            return handle;
        }

        @Override
        public void onConversationsEvent(ConversationEvent event) {
            if (event.getEventKind().equals(ConversationEventKind.NEW_CONVERSATION_MESSAGE)
                    && ((NewMessageConversationEvent) event).getConversationMessage().getConversationId()
                            .equals(conversationId))
                new ReloadMessagesHistoryAsyncTask().execute(Integer.valueOf(lastMineMessageIndex));
        }

        /** Implements ListAdapter */
        public View getView(int position, View convertView, ViewGroup parent) {
            _ConversationMessage msg = messageList.get(position);

            View view = convertView;
            if (view == null)
                view = mInflater.inflate(R.layout.chat_message_item, parent, false);

            TextView msgBodyView = (TextView) view.findViewById(R.id.chatMessageBodyTextView);
            Drawable background = getResources().getDrawable(R.drawable.bubble_colorizable); // Doing this here can create problems?
            int backgroundColor;
            if (msg.isMine())
                backgroundColor = 0xFFA8D324; // TODO: Make own color configurable
            else
                backgroundColor = ColorHash.getObjectColor(msg.getNick());
            background.setColorFilter(backgroundColor, PorterDuff.Mode.MULTIPLY);
            msgBodyView.setBackgroundDrawable(background);
            msgBodyView.setLinkTextColor(Util.opposeColor(backgroundColor));
            msgBodyView.setText(msg.getBody());
            Linkify.addLinks(msgBodyView, Pattern.compile(Util.URI_REG_EXP), ""); // This must be executed on UI thread

            TextView timeView = (TextView) view.findViewById(R.id.chatMessageTimeTextView);
            timeView.setText(msg.getTime());

            TextView nickView = (TextView) view.findViewById(R.id.chatMessageNickTextView);
            nickView.setText(msg.getNick());

            return view;
        }

        public int getViewTypeCount() {
            return 1;
        }

        public void registerDataSetObserver(DataSetObserver observer) {
            observerSet.add(observer);
        }

        public void unregisterDataSetObserver(DataSetObserver observer) {
            observerSet.remove(observer);
        }

        public boolean areAllItemsEnabled() {
            return true;
        }

        public boolean isEnabled(int position) {
            return true;
        }

        public boolean hasStableIds() {
            return false;
        }

        public boolean isEmpty() {
            return messageList.isEmpty();
        }

        public int getCount() {
            return messageList.size();
        }

        public int getItemViewType(int position) {
            return 0;
        }

        public long getItemId(int position) {
            return 0;
        }

        public Object getItem(int position) {
            return messageList.get(position);
        }

        private Set<DataSetObserver> observerSet = new HashSet<DataSetObserver>();

        /**
         * Notify data observer that fresh data are available
         */
        public void notifyObservers() {
            for (DataSetObserver obs : observerSet)
                obs.onChanged();
        }
    }

    private class _ConversationMessage {
        /**
         * This is executed in the AsyncTask thread so we can do work here
         * @param msg
         */
        public _ConversationMessage(RsConversationService.ConversationMessage msg) {
            Spanned spn = Html.fromHtml(msg.getMessageString(), chatImageGetter, null);
            msgBody = spn;
            nick = msg.getAuthorString();
            isMine = getConnectedServer().mRsPeersService.getOwnPerson().getName().equals(nick);

            if (msg.hasTime())
                time = new SimpleDateFormat(msg.getDefaultTimeFormat()).format(new Date(msg.getTime()));
            else
                time = "";
        }

        /**
         * Those methods are called in the UI thread so should be faster as possible
         */
        public String getNick() {
            return nick;
        }

        public Spanned getBody() {
            return msgBody;
        }

        public boolean isMine() {
            return isMine;
        }

        public String getTime() {
            return time;
        }

        private final Spanned msgBody;
        private final boolean isMine;
        private final String time;
        private final String nick;
    }

    private void initializeConversation() {
        if (isUserVisible() && isBound()) {
            RsConversationService rsConversationService = getConnectedServer().mRsConversationService;
            rsConversationService.cancelNotificationForConversation(conversationId);
            rsConversationService.disableNotificationForConversation(conversationId);
            lastMineMessageIndex = 0;
            adapter.new ReloadMessagesHistoryAsyncTask().execute(Integer.valueOf(lastMineMessageIndex));
        }
    }

    /**
     * Message sending stuff
     */

    private void sendChatMsg(View v) {
        if (isBound()) {
            EditText et = (EditText) getActivity().findViewById(R.id.chatFragmentMessageEditText);
            String inputText = et.getText().toString();

            if (inputText.length() > 0) {
                String msgText;
                if (inputText.equals("a"))
                    msgText = "<span><span style=\"font-family:\'\';font-size:8.25pt;font-weight:400;font-style:normal;\"><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAA7EAAAOxAGVKw4bAAACzklEQVRIibVWS08TURQ+dzqFKZROpg+balsKpE1jiUSQh0gwggsTYhpZqgtjMHFjTFTcuNOd8hPc4MLEBcaFEIORhYaoSSVIRYkgEKChQCktbaXv62JKAXs7w4z128y55/F9d86dMzOABTG59BTjbLFoMh2d9j0XZgDhcOj34tz6SH6ZziQy2WR+ObUymEzHhBkQxhgE4fU9Uyl1vzZe+8MTO6kgAFSUHTGxp6zaTlrB1Bl6hMtFBIKx2bGZe4Hod2LUxDZ3OR9XMWaZAqthz4i3L5WJCdQzSu3FhkFdpbNYAlUsEIn73ny7mcrEWmvu1ujPFyY0227XGi7EU8ER7414akuywPjcw0Q6DAD6qnoNYy1M0KtdrKoaAGIJ/+f5J8V4yC3ais2+8PQAiJz/3jYRfbXtfUWZgRAiFswHRg/PDgBZnF7cfEfWJnoD0enDs+dKIuSSvRYtBEZ9oU82XbeZO+MPe+KpkCQBlVJrZBsXAm99oY82XZeZ6zggsBz8MOy9DgAIUb2NL8fnHvnDXyQJmLn2Bkvf8NQuyckhQ1U95Fu0FpnkDYyz69uTkqjzWNveRxL5ytuEMxB9eRwGeZKic1Aq/C8BDBkBgRK0CARahHFGHicCRaGzlC1CiODMCVCIIF4SkO6AuBOp2OXICejVrnxIrz4uj9Ogri8kofmLVXu2yzmwsjVerTtnYpvlCVi0nd3OgeUcScsBAQBwGN0Oo5u3EcjoEgIAu9Ft3yXhQX6KBL6xxaBTk0toore1tl9BlYd25qMJ/2b0B+/UMGau0g4Aq2FPMh3hnce40zTFaCvsTbZbRCqR35afa6/GZvp5+4T5WnvdAwAYmujdiHh55+XWMQ1jEWAQGTTRw8A4K5wgZ5IlPQCyBKSMvYhAuZLbs2n2LwMhqpzW/JOAhetwGC9RiDaxTa6jV3hnS80djcpKK1RttfeZfTsg4g/+D5MwF/zpFwAAAABJRU5ErkJggg==\"/></span></span>"; // Easter egg
                else
                    msgText = StringEscapeUtils.escapeHtml(inputText);

                ConversationMessage msg = ConversationMessage.Factory.newConversationMessage(conversationId);
                msg.setMessageString(msgText);
                getConnectedServer().mRsConversationService.sendConversationMessage(msg);

                et.setText("");

                autoScrollTo(conversationMessageListView.getCount(), true);
            }
        } else
            Log.e(TAG(), "sendChatMsg(View v) cannot send message without connection to rsProxy");
    }

    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        boolean enterPressed = (event.getAction() == KeyEvent.ACTION_DOWN)
                & (event.getKeyCode() == KeyEvent.KEYCODE_ENTER);
        if (enterPressed)
            sendChatMsg(null);
        return enterPressed;
    }

    /**
     * Autoscroll stuff
     */
    private boolean humanScrolled = false;
    private int lastMineMessageIndex = 0;

    @Override
    public void onScrollStateChanged(AbsListView absListView, int i) {
    }

    @Override
    public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (lastMineMessageIndex < firstVisibleItem)
            moreMessageUpIndicator.setVisibility(View.VISIBLE);
        else
            moreMessageUpIndicator.setVisibility(View.INVISIBLE);
    }

    private void autoScrollTo(int position, boolean resetHumanScrolled) {
        if (resetHumanScrolled)
            humanScrolled = false;
        if (!humanScrolled) {
            conversationMessageListView.smoothScrollToPosition(position);
            conversationMessageListView.setSelection(position);
        }
    }

    private class GoDownButtonListener implements View.OnClickListener {
        public void onClick(View v) {
            autoScrollTo(conversationMessageListView.getCount() - 1, true);
        }
    }

    private class GoUpButtonListener implements View.OnClickListener {
        public void onClick(View v) {
            autoScrollTo(lastMineMessageIndex, true);
            humanScrolled = true;
        }
    }

    /**
     * ExtraMenu stuff
     */
    private View sendExtraMenu;

    private final class OnShowSendExtraLongClickListener implements View.OnLongClickListener {
        @Override
        public boolean onLongClick(View view) {
            sendExtraMenu.setVisibility(View.VISIBLE);
            return true;
        }
    }

    private final static int SELECT_IMAGE_INTENT_REQUEST_CODE = 1;

    private final class OnAddImageClickListener implements View.OnClickListener {
        @Override
        public void onClick(View view) {
            sendExtraMenu.setVisibility(View.INVISIBLE);

            Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
            photoPickerIntent.setType("image/*");
            startActivityForResult(photoPickerIntent, SELECT_IMAGE_INTENT_REQUEST_CODE);
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode != Activity.RESULT_OK)
            return;

        switch (requestCode) {
        case SELECT_IMAGE_INTENT_REQUEST_CODE: {
            new SendImageAsyncTask().execute(data.getData());
            break;
        }
        }
    }

    private final class SendImageAsyncTask extends AsyncTask<Uri, Void, ConversationMessage> {
        ContentResolver mContentResolver;

        @Override
        public void onPreExecute() {
            mContentResolver = getActivity().getContentResolver();
        }

        @Override
        protected ConversationMessage doInBackground(Uri... uris) {
            Bitmap bitmapImage;
            try {
                bitmapImage = Util.loadFittingBitmap(mContentResolver, uris[0], CONVERSATION_IMAGE_MAX_DIMENSION);
            } catch (FileNotFoundException e) {
                return null;
            }

            StringBuilder msgBodyBuilder = new StringBuilder(
                    CONVERSATION_IMAGE_MAX_DIMENSION * CONVERSATION_IMAGE_MAX_DIMENSION);
            msgBodyBuilder.append("<img src=\"data:image/png;base64,");
            msgBodyBuilder.append(Util.encodeTobase64(bitmapImage, Bitmap.CompressFormat.PNG, 100));
            bitmapImage.recycle(); // Mark the bitmap as garbage
            msgBodyBuilder.append("\"/>");

            ConversationMessage msg = ConversationMessage.Factory.newConversationMessage(conversationId);
            msg.setMessageString(msgBodyBuilder.toString());

            return msg;
        }

        @Override
        public void onPostExecute(ConversationMessage msg) {
            if (msg != null)
                getConnectedServer().mRsConversationService.sendConversationMessage(msg);
        }
    }

    private static final String CONFERSATION_INFO_DIALOG_FRAGMENT_TAG = "org.retroshare.android.ConversationInfoDialogFragment";

    private final class ShowConversationInfoLongClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            sendExtraMenu.setVisibility(View.INVISIBLE);

            RsConversationService.ConversationInfo info = getConnectedServer().mRsConversationService
                    .getConversationInfo(conversationId);
            Bundle args = new Bundle(1);
            args.putParcelable(ConversationInfoDialogFragment.CONVERSATION_INFO_EXTRA, info);
            ConversationInfoDialogFragment df = new ConversationInfoDialogFragment(getActivity());
            df.setArguments(args);
            df.show(getFragmentManager(), CONFERSATION_INFO_DIALOG_FRAGMENT_TAG);
        }
    }

    public ConversationFragment() {
        super();
    }
}