im.neon.adapters.VectorMessagesAdapter.java Source code

Java tutorial

Introduction

Here is the source code for im.neon.adapters.VectorMessagesAdapter.java

Source

/*
 * Copyright 2015 OpenMarket Ltd
 * Copyright 2017 Vector Creations Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.neon.adapters;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.text.format.DateUtils;
import org.matrix.androidsdk.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView;

import org.matrix.androidsdk.MXSession;
import org.matrix.androidsdk.adapters.MessageRow;
import org.matrix.androidsdk.adapters.MessagesAdapter;
import org.matrix.androidsdk.crypto.data.MXDeviceInfo;
import org.matrix.androidsdk.data.store.IMXStore;
import org.matrix.androidsdk.data.Room;
import org.matrix.androidsdk.data.RoomState;
import org.matrix.androidsdk.db.MXMediasCache;
import org.matrix.androidsdk.listeners.IMXMediaDownloadListener;
import org.matrix.androidsdk.listeners.IMXMediaUploadListener;
import org.matrix.androidsdk.rest.model.EncryptedEventContent;
import org.matrix.androidsdk.rest.model.Event;
import org.matrix.androidsdk.rest.model.Message;
import org.matrix.androidsdk.rest.model.PowerLevels;
import org.matrix.androidsdk.rest.model.ReceiptData;
import org.matrix.androidsdk.rest.model.RoomMember;
import org.matrix.androidsdk.util.JsonUtils;

import im.neon.VectorApp;
import im.neon.R;
import im.neon.util.VectorUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;

/**
 * An adapter which can display room information.
 */
public class VectorMessagesAdapter extends MessagesAdapter {

    private static final String LOG_TAG = "VMessagesAdapter";

    public interface VectorMessagesAdapterActionsListener {
        /**
         * An action has been  triggered on an event.
         * @param event the event.
         * @param textMsg the text message
         * @param action an action ic_action_vector_XXX
         */
        void onEventAction(final Event event, final String textMsg, final int action);

        /**
         * the user taps on the e2e icon
         * @param event the event
         * @param deviceInfo the deviceinfo
         */
        void onE2eIconClick(final Event event, final MXDeviceInfo deviceInfo);
    }

    // an event is highlighted when the user taps on it
    private String mHighlightedEventId;

    // events listeners
    private VectorMessagesAdapterActionsListener mVectorMessagesAdapterEventsListener = null;

    // current date : used to compute the day header
    private Date mReferenceDate = new Date();

    // day date of each message
    // the hours, minutes and seconds are removed
    private ArrayList<Date> mMessagesDateList = new ArrayList<>();

    // when the adapter is used in search mode
    // the searched message should be highlighted
    private String mSearchedEventId = null;

    // formatted time by event id
    // it avoids computing them several times
    private final HashMap<String, String> mEventFormattedTsMap = new HashMap<>();

    // define the e2e icon to use for a dedicated eventId
    private HashMap<String, Integer> mE2eIconByEventId = new HashMap<>();

    // device info by device id
    private HashMap<String, MXDeviceInfo> mE2eDeviceByEventId = new HashMap<>();

    // true when the room is encrypted
    public boolean mIsRoomEncrypted;

    /**
     * Expanded constructor.
     */
    public VectorMessagesAdapter(MXSession session, Context context, int textResLayoutId, int imageResLayoutId,
            int noticeResLayoutId, int emoteRestLayoutId, int fileResLayoutId, int videoResLayoutId,
            MXMediasCache mediasCache) {

        super(session, context, textResLayoutId, imageResLayoutId, noticeResLayoutId, emoteRestLayoutId,
                fileResLayoutId, videoResLayoutId, mediasCache);
    }

    /**
     * Creates a messages adapter with the default layouts.
     */
    public VectorMessagesAdapter(MXSession session, Context context, MXMediasCache mediasCache) {
        super(session, context, R.layout.adapter_item_vector_message_text_emote_notice,
                R.layout.adapter_item_vector_message_image_video,
                R.layout.adapter_item_vector_message_text_emote_notice,
                R.layout.adapter_item_vector_message_text_emote_notice, R.layout.adapter_item_vector_message_file,
                R.layout.adapter_item_vector_message_image_video, mediasCache);
    }

    @Override
    public int getEncryptingMessageTextColor(Context context) {
        return context.getResources().getColor(R.color.vector_green_color);
    }

    /**
     * the parent fragment is paused.
     */
    public void onPause() {
        mEventFormattedTsMap.clear();
    }

    /**
     * Toggle the selection mode.
     * @param eventId the tapped eventID.
     */
    public void onEventTap(String eventId) {
        // the tap to select is only enabled when the adapter is not in search mode.
        if (!mIsSearchMode) {
            if (null == mHighlightedEventId) {
                mHighlightedEventId = eventId;
            } else {
                mHighlightedEventId = null;
            }
            notifyDataSetChanged();
        }
    }

    /**
     * Display a bar to the left of the message
     * @param eventId the event id
     */
    public void setSearchedEventId(String eventId) {
        mSearchedEventId = eventId;
    }

    /**
     * Cancel the message selection mode
     */
    public void cancelSelectionMode() {
        if (null != mHighlightedEventId) {
            mHighlightedEventId = null;
            notifyDataSetChanged();
        }
    }

    /**
     * @return true if there is a selected item.
     */
    public boolean isInSelectionMode() {
        return null != mHighlightedEventId;
    }

    /**
     * Define the events listener
     * @param listener teh events listener
     */
    public void setVectorMessagesAdapterActionsListener(VectorMessagesAdapterActionsListener listener) {
        mVectorMessagesAdapterEventsListener = listener;
    }

    /**
     * Provides the formatted timestamp to display.
     * null means that the timestamp text must be hidden.
     * @param event the event.
     * @return  the formatted timestamp to display.
     */
    @Override
    protected String getFormattedTimestamp(Event event) {
        String res = mEventFormattedTsMap.get(event.eventId);

        if (null != res) {
            return res;
        }

        if (event.isValidOriginServerTs()) {
            res = AdapterUtils.tsToString(mContext, event.getOriginServerTs(), true);
        } else {
            res = " ";
        }

        mEventFormattedTsMap.put(event.eventId, res);

        return res;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = super.getView(position, convertView, parent);

        if (null != view) {
            view.setBackgroundColor(Color.TRANSPARENT);
        }

        ImageView e2eIconView = (ImageView) view.findViewById(R.id.message_adapter_e2e_icon);
        View senderMargin = view.findViewById(R.id.e2e_sender_margin);
        View senderNameView = view.findViewById(R.id.messagesAdapter_sender);

        // GA issue
        if (position >= getCount()) {
            return view;
        }

        MessageRow row = getItem(position);
        final Event event = row.getEvent();

        if (mE2eIconByEventId.containsKey(event.eventId)) {
            senderMargin.setVisibility(senderNameView.getVisibility());
            e2eIconView.setVisibility(View.VISIBLE);
            e2eIconView.setImageResource(mE2eIconByEventId.get(event.eventId));

            int type = getItemViewType(position);

            if ((type == ROW_TYPE_IMAGE) || (type == ROW_TYPE_VIDEO)) {
                View bodyLayoutView = view.findViewById(org.matrix.androidsdk.R.id.messagesAdapter_body_layout);
                ViewGroup.MarginLayoutParams bodyLayout = (ViewGroup.MarginLayoutParams) bodyLayoutView
                        .getLayoutParams();
                ViewGroup.MarginLayoutParams e2eIconViewLayout = (ViewGroup.MarginLayoutParams) e2eIconView
                        .getLayoutParams();

                e2eIconViewLayout.setMargins(bodyLayout.leftMargin, e2eIconViewLayout.topMargin,
                        e2eIconViewLayout.rightMargin, e2eIconViewLayout.bottomMargin);
                bodyLayout.setMargins(4, bodyLayout.topMargin, bodyLayout.rightMargin, bodyLayout.bottomMargin);
                e2eIconView.setLayoutParams(e2eIconViewLayout);
                bodyLayoutView.setLayoutParams(bodyLayout);
            }

            e2eIconView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (null != mVectorMessagesAdapterEventsListener) {
                        mVectorMessagesAdapterEventsListener.onE2eIconClick(event,
                                mE2eDeviceByEventId.get(event.eventId));
                    }
                }
            });
        } else {
            e2eIconView.setVisibility(View.GONE);
            senderMargin.setVisibility(View.GONE);
        }

        return view;
    }

    /**
     * Retrieves the MXDevice info from an event id
     * @param eventId the event id
     * @return the linked device info, null it it does not exist.
     */
    public MXDeviceInfo getDeviceInfo(String eventId) {
        MXDeviceInfo deviceInfo = null;

        if (null != eventId) {
            deviceInfo = mE2eDeviceByEventId.get(eventId);
        }

        return deviceInfo;
    }

    @Override
    protected void setTypingVisibility(View avatarLayoutView, int status) {
    }

    @Override
    protected void refreshPresenceRing(ImageView presenceView, String userId) {
    }

    @Override
    protected void loadMemberAvatar(ImageView avatarView, RoomMember member, String userId, String displayName,
            String url) {
        if (!mSession.isAlive()) {
            return;
        }

        // if there is no preferred display name, use the member one
        if (TextUtils.isEmpty(displayName) && (null != member)) {
            displayName = member.displayname;
        }

        if ((member != null) && (null == url)) {
            url = member.avatarUrl;
        }

        if (null != member) {
            VectorUtils.loadUserAvatar(mContext, mSession, avatarView, url, member.getUserId(), displayName);
        } else {
            VectorUtils.loadUserAvatar(mContext, mSession, avatarView, url, userId, displayName);
        }
    }

    /**
     * Found the dedicated icon to display for each event id
     */
    private void manageCryptoEvents() {
        HashMap<String, Integer> e2eIconByEventId = new HashMap<>();
        HashMap<String, MXDeviceInfo> e2eDeviceInfoByEventId = new HashMap<>();

        if (mIsRoomEncrypted && mSession.isCryptoEnabled()) {
            // the key is "userid_deviceid"
            for (int index = 0; index < this.getCount(); index++) {
                MessageRow row = getItem(index);
                Event event = row.getEvent();

                // oneself event
                if (event.mSentState != Event.SentState.SENT) {
                    e2eIconByEventId.put(event.eventId, R.drawable.e2e_verified);
                }
                // not encrypted event
                else if (!event.isEncrypted()) {
                    e2eIconByEventId.put(event.eventId, R.drawable.e2e_unencrypted);
                }
                // in error cases, do not display
                else if (null != event.getCryptoError()) {
                    e2eIconByEventId.put(event.eventId, R.drawable.e2e_blocked);
                } else {
                    EncryptedEventContent encryptedEventContent = JsonUtils
                            .toEncryptedEventContent(event.getWireContent().getAsJsonObject());

                    if (TextUtils.equals(mSession.getCredentials().deviceId, encryptedEventContent.device_id)
                            && TextUtils.equals(mSession.getMyUserId(), event.getSender())) {
                        e2eIconByEventId.put(event.eventId, R.drawable.e2e_verified);
                        MXDeviceInfo deviceInfo = mSession.getCrypto().deviceWithIdentityKey(
                                encryptedEventContent.sender_key, event.getSender(),
                                encryptedEventContent.algorithm);

                        if (null != deviceInfo) {
                            e2eDeviceInfoByEventId.put(event.eventId, deviceInfo);
                        }

                    } else {
                        MXDeviceInfo deviceInfo = mSession.getCrypto().deviceWithIdentityKey(
                                encryptedEventContent.sender_key, event.getSender(),
                                encryptedEventContent.algorithm);

                        if (null != deviceInfo) {
                            e2eDeviceInfoByEventId.put(event.eventId, deviceInfo);
                            if (deviceInfo.isVerified()) {
                                e2eIconByEventId.put(event.eventId, R.drawable.e2e_verified);
                            } else if (deviceInfo.isBlocked()) {
                                e2eIconByEventId.put(event.eventId, R.drawable.e2e_blocked);
                            } else {
                                e2eIconByEventId.put(event.eventId, R.drawable.e2e_warning);
                            }
                        } else {
                            e2eIconByEventId.put(event.eventId, R.drawable.e2e_warning);
                        }
                    }
                }
            }
        }

        mE2eDeviceByEventId = e2eDeviceInfoByEventId;
        mE2eIconByEventId = e2eIconByEventId;
    }

    @Override
    public void notifyDataSetChanged() {
        // the event with invalid timestamp must be pushed at the end of the history
        this.setNotifyOnChange(false);
        List<MessageRow> undeliverableEvents = new ArrayList<>();

        for (int i = 0; i < getCount(); i++) {
            MessageRow row = getItem(i);
            Event event = row.getEvent();

            if ((null != event) && (!event.isValidOriginServerTs() || event.isUnkownDevice())) {
                undeliverableEvents.add(row);
                removeRow(row);
                i--;
            }
        }

        if (undeliverableEvents.size() > 0) {
            try {
                Collections.sort(undeliverableEvents, new Comparator<MessageRow>() {
                    @Override
                    public int compare(MessageRow m1, MessageRow m2) {
                        long diff = m1.getEvent().getOriginServerTs() - m2.getEvent().getOriginServerTs();
                        return (diff > 0) ? +1 : ((diff < 0) ? -1 : 0);
                    }
                });
            } catch (Exception e) {
                Log.e(LOG_TAG, "## notifyDataSetChanged () : failed to sort undeliverableEvents " + e.getMessage());
            }

            this.addAll(undeliverableEvents);
        }

        this.setNotifyOnChange(true);

        // build messages timestamps
        ArrayList<Date> dates = new ArrayList<>();

        Date latestDate = AdapterUtils.zeroTimeDate(new Date());

        for (int index = 0; index < this.getCount(); index++) {
            MessageRow row = getItem(index);
            Event event = row.getEvent();

            if (event.isValidOriginServerTs()) {
                latestDate = AdapterUtils.zeroTimeDate(new Date(event.getOriginServerTs()));
            }

            dates.add(latestDate);
        }

        synchronized (this) {
            mMessagesDateList = dates;
            mReferenceDate = new Date();
        }

        manageCryptoEvents();

        //  do not refresh the room when the application is in background
        // on large rooms, it drains a lot of battery
        if (!VectorApp.isAppInBackground()) {
            super.notifyDataSetChanged();
        }
    }

    /**
     * Converts a difference of days to a string.
     * @param date the date to display
     * @param nbrDays the number of days between the reference days
     * @return the date text
     */
    private String dateDiff(Date date, long nbrDays) {
        if (nbrDays == 0) {
            return mContext.getResources().getString(R.string.today);
        } else if (nbrDays == 1) {
            return mContext.getResources().getString(R.string.yesterday);
        } else if (nbrDays < 7) {
            return (new SimpleDateFormat("EEEE", AdapterUtils.getLocale(mContext))).format(date);
        } else {
            int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_ALL
                    | DateUtils.FORMAT_SHOW_WEEKDAY;

            Formatter f = new Formatter(new StringBuilder(50), AdapterUtils.getLocale(mContext));
            return DateUtils.formatDateRange(mContext, f, date.getTime(), date.getTime(), flags).toString();
        }
    }

    protected String headerMessage(int position) {
        Date prevMessageDate = null;
        Date messageDate = null;

        synchronized (this) {
            if ((position > 0) && (position < mMessagesDateList.size())) {
                prevMessageDate = mMessagesDateList.get(position - 1);
            }
            if (position < mMessagesDateList.size()) {
                messageDate = mMessagesDateList.get(position);
            }
        }

        // sanity check
        if (null == messageDate) {
            return null;
        }

        // same day or get the oldest message
        if ((null != prevMessageDate) && 0 == (prevMessageDate.getTime() - messageDate.getTime())) {
            return null;
        }

        return dateDiff(messageDate, (mReferenceDate.getTime() - messageDate.getTime()) / AdapterUtils.MS_IN_DAY);
    }

    @Override
    protected boolean isAvatarDisplayedOnRightSide(Event event) {
        return false;
    }

    @Override
    protected void refreshReceiverLayout(final LinearLayout receiversLayout, final boolean leftAlign,
            final String eventId, final RoomState roomState) {
        if (null != receiversLayout) {
            // replaced by displayReadReceipts
            receiversLayout.setVisibility(View.GONE);
        }
    }

    @Override
    protected void onTypingUsersUpdate() {
        // the typing users are now displayed in a dedicated area in the activity
    }

    /**
     * Display the read receipts within the dedicated vector layout.
     * Console application displays them on the message side.
     * Vector application displays them in a dedicated line under the message
     * @param avatarsListView the read receipts layout
     * @param eventId the event Id.
     * @param roomState the room state.
     */
    private void displayReadReceipts(final View avatarsListView, final String eventId, final RoomState roomState) {
        if (!mSession.isAlive()) {
            return;
        }

        IMXStore store = mSession.getDataHandler().getStore();

        // sanity check
        if (null == roomState) {
            avatarsListView.setVisibility(View.GONE);
            return;
        }

        // hide the read receipts until there is a way to retrieve them
        // without triggering a request per message
        if (mIsPreviewMode) {
            avatarsListView.setVisibility(View.GONE);
            return;
        }

        List<ReceiptData> receipts = store.getEventReceipts(roomState.roomId, eventId, true, true);

        // if there is no receipt to display
        // hide the dedicated layout
        if ((null == receipts) || (0 == receipts.size())) {
            avatarsListView.setVisibility(View.GONE);
            return;
        }

        avatarsListView.setVisibility(View.VISIBLE);

        ArrayList<View> imageViews = new ArrayList<>();

        imageViews.add(avatarsListView.findViewById(R.id.message_avatar_receipt_1)
                .findViewById(org.matrix.androidsdk.R.id.avatar_img));
        imageViews.add(avatarsListView.findViewById(R.id.message_avatar_receipt_2)
                .findViewById(org.matrix.androidsdk.R.id.avatar_img));
        imageViews.add(avatarsListView.findViewById(R.id.message_avatar_receipt_3)
                .findViewById(org.matrix.androidsdk.R.id.avatar_img));
        imageViews.add(avatarsListView.findViewById(R.id.message_avatar_receipt_4)
                .findViewById(org.matrix.androidsdk.R.id.avatar_img));
        imageViews.add(avatarsListView.findViewById(R.id.message_avatar_receipt_5)
                .findViewById(org.matrix.androidsdk.R.id.avatar_img));

        TextView moreText = (TextView) avatarsListView.findViewById(R.id.message_more_than_expected);

        int index = 0;
        int bound = Math.min(receipts.size(), imageViews.size());

        for (; index < bound; index++) {
            final ReceiptData r = receipts.get(index);
            RoomMember member = roomState.getMember(r.userId);
            ImageView imageView = (ImageView) imageViews.get(index);

            imageView.setVisibility(View.VISIBLE);
            imageView.setTag(null);

            if (null != member) {
                VectorUtils.loadRoomMemberAvatar(mContext, mSession, imageView, member);
            } else {
                // should never happen
                VectorUtils.loadUserAvatar(mContext, mSession, imageView, null, r.userId, r.userId);
            }
        }

        moreText.setVisibility((receipts.size() <= imageViews.size()) ? View.GONE : View.VISIBLE);
        moreText.setText((receipts.size() - imageViews.size()) + "+");

        for (; index < imageViews.size(); index++) {
            imageViews.get(index).setVisibility(View.INVISIBLE);
        }

        if (receipts.size() > 0) {
            avatarsListView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (null != mMessagesAdapterEventsListener) {
                        mMessagesAdapterEventsListener.onMoreReadReceiptClick(eventId);
                    }
                }
            });
        } else {
            avatarsListView.setOnClickListener(null);
        }
    }

    /**
     * The user taps on the action icon.
     * @param event the selected event.
     * @param textMsg the event text
     * @param anchorView the popup anchor.
     */
    @SuppressLint("NewApi")
    private void onMessageClick(final Event event, final String textMsg, final View anchorView) {
        final PopupMenu popup = (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
                ? new PopupMenu(mContext, anchorView, Gravity.END)
                : new PopupMenu(mContext, anchorView);

        popup.getMenuInflater().inflate(R.menu.vector_room_message_settings, popup.getMenu());

        // force to display the icons
        try {
            Field[] fields = popup.getClass().getDeclaredFields();
            for (Field field : fields) {
                if ("mPopup".equals(field.getName())) {
                    field.setAccessible(true);
                    Object menuPopupHelper = field.get(popup);
                    Class<?> classPopupHelper = Class.forName(menuPopupHelper.getClass().getName());
                    Method setForceIcons = classPopupHelper.getMethod("setForceShowIcon", boolean.class);
                    setForceIcons.invoke(menuPopupHelper, true);
                    break;
                }
            }
        } catch (Exception e) {
            Log.e(LOG_TAG, "onMessageClick : force to display the icons failed " + e.getLocalizedMessage());
        }

        Menu menu = popup.getMenu();

        // hide entries
        for (int i = 0; i < menu.size(); i++) {
            menu.getItem(i).setVisible(false);
        }

        menu.findItem(R.id.ic_action_view_source).setVisible(true);
        menu.findItem(R.id.ic_action_vector_permalink).setVisible(true);

        if (!TextUtils.isEmpty(textMsg)) {
            menu.findItem(R.id.ic_action_vector_copy).setVisible(true);
            menu.findItem(R.id.ic_action_vector_quote).setVisible(true);
        }

        if (event.isUploadingMedias(mMediasCache)) {
            menu.findItem(R.id.ic_action_vector_cancel_upload).setVisible(true);
        }

        if (event.isDownloadingMedias(mMediasCache)) {
            menu.findItem(R.id.ic_action_vector_cancel_download).setVisible(true);
        }

        if (event.canBeResent()) {
            menu.findItem(R.id.ic_action_vector_resend_message).setVisible(true);

            if (event.isUndeliverable() || event.isUnkownDevice()) {
                menu.findItem(R.id.ic_action_vector_redact_message).setVisible(true);
            }
        } else if (event.mSentState == Event.SentState.SENT) {

            // test if the event can be redacted
            boolean canBeRedacted = !mIsPreviewMode
                    && !TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE_ENCRYPTION);

            if (canBeRedacted) {
                // oneself message -> can redact it
                if (TextUtils.equals(event.sender, mSession.getMyUserId())) {
                    canBeRedacted = true;
                } else {
                    // need the mininum power level to redact an event
                    Room room = mSession.getDataHandler().getRoom(event.roomId);

                    if ((null != room) && (null != room.getLiveState().getPowerLevels())) {
                        PowerLevels powerLevels = room.getLiveState().getPowerLevels();
                        canBeRedacted = powerLevels.getUserPowerLevel(mSession.getMyUserId()) >= powerLevels.redact;
                    }
                }
            }

            menu.findItem(R.id.ic_action_vector_redact_message).setVisible(canBeRedacted);

            if (Event.EVENT_TYPE_MESSAGE.equals(event.getType())) {
                Message message = JsonUtils.toMessage(event.getContentAsJsonObject());

                // share / forward the message
                menu.findItem(R.id.ic_action_vector_share).setVisible(true);
                menu.findItem(R.id.ic_action_vector_forward).setVisible(true);

                // save the media in the downloads directory
                if (Message.MSGTYPE_IMAGE.equals(message.msgtype) || Message.MSGTYPE_VIDEO.equals(message.msgtype)
                        || Message.MSGTYPE_FILE.equals(message.msgtype)) {
                    menu.findItem(R.id.ic_action_vector_save).setVisible(true);
                }

                // offer to report a message content
                menu.findItem(R.id.ic_action_vector_report)
                        .setVisible(!mIsPreviewMode && !TextUtils.equals(event.sender, mSession.getMyUserId()));
            }

        }

        // e2e
        menu.findItem(R.id.ic_action_device_verification).setVisible(mE2eIconByEventId.containsKey(event.eventId));

        // display the menu
        popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(final MenuItem item) {
                // warn the listener
                if (null != mVectorMessagesAdapterEventsListener) {
                    mVectorMessagesAdapterEventsListener.onEventAction(event, textMsg, item.getItemId());
                }

                // disable the selection
                mHighlightedEventId = null;
                notifyDataSetChanged();

                return true;
            }
        });

        // fix an issue reported by GA
        try {
            popup.show();
        } catch (Exception e) {
            Log.e(LOG_TAG, " popup.show failed " + e.getMessage());
        }
    }

    /**
     * Manage the select mode i.e highlight an item when the user tap on it
     * @param contentView the cell view.
     * @param event the linked event
     */
    private void manageSelectionMode(final View contentView, final Event event) {
        final String eventId = event.eventId;

        boolean isInSelectionMode = (null != mHighlightedEventId);
        boolean isHighlighted = TextUtils.equals(eventId, mHighlightedEventId);

        // display the action icon when selected
        contentView.findViewById(R.id.messagesAdapter_action_image)
                .setVisibility(isHighlighted ? View.VISIBLE : View.GONE);

        float alpha = (!isInSelectionMode || isHighlighted) ? 1.0f : 0.2f;

        // the message body is dimmed when not selected
        contentView.findViewById(R.id.messagesAdapter_body_view).setAlpha(alpha);
        contentView.findViewById(R.id.messagesAdapter_avatars_list).setAlpha(alpha);

        TextView tsTextView = (TextView) contentView
                .findViewById(org.matrix.androidsdk.R.id.messagesAdapter_timestamp);
        if (isInSelectionMode && isHighlighted) {
            tsTextView.setVisibility(View.VISIBLE);
        }

        contentView.findViewById(org.matrix.androidsdk.R.id.message_timestamp_layout_right)
                .setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (TextUtils.equals(eventId, mHighlightedEventId)) {
                            onMessageClick(event, getEventText(contentView),
                                    contentView.findViewById(R.id.messagesAdapter_action_anchor));
                        } else {
                            onEventTap(eventId);
                        }
                    }
                });

        contentView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if (!mIsSearchMode) {
                    onMessageClick(event, getEventText(contentView),
                            contentView.findViewById(R.id.messagesAdapter_action_anchor));
                    mHighlightedEventId = eventId;
                    notifyDataSetChanged();
                    return true;
                }

                return false;
            }
        });
    }

    @Override
    protected boolean mergeView(Event event, int position, boolean shouldBeMerged) {
        if (shouldBeMerged) {
            shouldBeMerged = null == headerMessage(position);
        }

        return shouldBeMerged && !event.isCallEvent();
    }

    @Override
    protected boolean isMergeableEvent(Event event) {
        return super.isMergeableEvent(event) && !event.isCallEvent();
    }

    /**
     * Return the text displayed in a convertView in the chat history.
     * @param contentView the cell view
     * @return the displayed text.
     */
    private String getEventText(View contentView) {
        String text = null;

        if (null != contentView) {
            TextView bodyTextView = (TextView) contentView
                    .findViewById(org.matrix.androidsdk.R.id.messagesAdapter_body);

            if (null != bodyTextView) {
                text = bodyTextView.getText().toString();
            }
        }

        return text;
    }

    @Override
    protected void addContentViewListeners(final View convertView, final View contentView, final int position) {
        contentView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (null != mMessagesAdapterEventsListener) {
                    // GA issue
                    if (position < getCount()) {
                        mMessagesAdapterEventsListener.onContentClick(position);
                    }
                }
            }
        });

        contentView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                // GA issue
                if (position < getCount()) {
                    MessageRow row = getItem(position);
                    Event event = row.getEvent();

                    if (!mIsSearchMode) {
                        onMessageClick(event, getEventText(contentView),
                                convertView.findViewById(R.id.messagesAdapter_action_anchor));
                        mHighlightedEventId = event.eventId;
                        notifyDataSetChanged();
                        return true;
                    }
                }

                return true;
            }
        });
    }

    @Override
    protected boolean manageSubView(int position, View convertView, View subView, int msgType) {
        MessageRow row = getItem(position);
        Event event = row.getEvent();

        // mother class implementation
        boolean isMergedView = super.manageSubView(position, convertView, subView, msgType);

        // remove the message separator when it is not required
        View view = convertView.findViewById(org.matrix.androidsdk.R.id.messagesAdapter_message_separator);
        if (null != view) {
            View line = convertView.findViewById(org.matrix.androidsdk.R.id.messagesAdapter_message_separator_line);

            if (null != line) {
                line.setBackgroundColor(Color.TRANSPARENT);
            }
        }

        // display the day separator
        View headerLayout = convertView.findViewById(org.matrix.androidsdk.R.id.messagesAdapter_message_header);
        if (null != headerLayout) {
            String header = headerMessage(position);

            if (null != header) {
                TextView headerText = (TextView) convertView
                        .findViewById(org.matrix.androidsdk.R.id.messagesAdapter_message_header_text);
                headerText.setText(header);
                headerLayout.setVisibility(View.VISIBLE);

                View topHeaderMargin = headerLayout.findViewById(R.id.messagesAdapter_message_header_top_margin);
                topHeaderMargin.setVisibility((0 == position) ? View.GONE : View.VISIBLE);
            } else {
                headerLayout.setVisibility(View.GONE);
            }
        }

        // the timestamp is hidden except for the latest message and when there is no search
        View rightTsTextLayout = convertView
                .findViewById(org.matrix.androidsdk.R.id.message_timestamp_layout_right);

        if (null != rightTsTextLayout) {
            TextView tsTextView = (TextView) rightTsTextLayout
                    .findViewById(org.matrix.androidsdk.R.id.messagesAdapter_timestamp);

            if (null != tsTextView) {
                tsTextView.setVisibility(
                        (((position + 1) == this.getCount()) || mIsSearchMode) ? View.VISIBLE : View.GONE);
            }
        }

        // On Vector application, the read receipts are displayed in a dedicated line under the message
        View avatarsListView = convertView.findViewById(R.id.messagesAdapter_avatars_list);

        if (null != avatarsListView) {
            displayReadReceipts(avatarsListView, event.eventId, row.getRoomState());
        }

        // selection mode
        manageSelectionMode(convertView, event);

        // search message mode
        View highlightMakerView = convertView.findViewById(R.id.messagesAdapter_highlight_message_marker);

        if (null != highlightMakerView) {
            if (TextUtils.equals(mSearchedEventId, event.eventId)) {
                View avatarView = convertView
                        .findViewById(org.matrix.androidsdk.R.id.messagesAdapter_roundAvatar_left);
                ViewGroup.LayoutParams avatarLayout = avatarView.getLayoutParams();

                // align marker view with the message
                ViewGroup.MarginLayoutParams highlightMakerLayout = (ViewGroup.MarginLayoutParams) highlightMakerView
                        .getLayoutParams();

                if (isMergedView) {
                    highlightMakerLayout.setMargins(avatarLayout.width + 5, highlightMakerLayout.topMargin, 5,
                            highlightMakerLayout.bottomMargin);

                } else {
                    highlightMakerLayout.setMargins(5, highlightMakerLayout.topMargin, 5,
                            highlightMakerLayout.bottomMargin);
                }

                highlightMakerView.setLayoutParams(highlightMakerLayout);

                // move left the body
                View bodyLayoutView = convertView
                        .findViewById(org.matrix.androidsdk.R.id.messagesAdapter_body_layout);
                ViewGroup.MarginLayoutParams bodyLayout = (ViewGroup.MarginLayoutParams) bodyLayoutView
                        .getLayoutParams();
                bodyLayout.setMargins(4, bodyLayout.topMargin, 4, bodyLayout.bottomMargin);

                highlightMakerView.setBackgroundColor(ContextCompat.getColor(mContext, R.color.vector_green_color));
            } else {
                highlightMakerView
                        .setBackgroundColor(ContextCompat.getColor(mContext, android.R.color.transparent));
            }
        }

        // download / upload progress layout
        if ((ROW_TYPE_IMAGE == msgType) || (ROW_TYPE_FILE == msgType) || (ROW_TYPE_VIDEO == msgType)) {
            View bodyLayoutView = convertView.findViewById(org.matrix.androidsdk.R.id.messagesAdapter_body_layout);
            ViewGroup.MarginLayoutParams bodyLayoutParams = (ViewGroup.MarginLayoutParams) bodyLayoutView
                    .getLayoutParams();
            int marginLeft = bodyLayoutParams.leftMargin;

            View downloadProgressLayout = convertView
                    .findViewById(org.matrix.androidsdk.R.id.content_download_progress_layout);

            if (null != downloadProgressLayout) {
                ViewGroup.MarginLayoutParams downloadProgressLayoutParams = (ViewGroup.MarginLayoutParams) downloadProgressLayout
                        .getLayoutParams();
                downloadProgressLayoutParams.setMargins(marginLeft, downloadProgressLayoutParams.topMargin,
                        downloadProgressLayoutParams.rightMargin, downloadProgressLayoutParams.bottomMargin);
                downloadProgressLayout.setLayoutParams(downloadProgressLayoutParams);
            }

            View uploadProgressLayout = convertView
                    .findViewById(org.matrix.androidsdk.R.id.content_upload_progress_layout);

            if (null != uploadProgressLayout) {
                ViewGroup.MarginLayoutParams uploadProgressLayoutParams = (ViewGroup.MarginLayoutParams) uploadProgressLayout
                        .getLayoutParams();
                uploadProgressLayoutParams.setMargins(marginLeft, uploadProgressLayoutParams.topMargin,
                        uploadProgressLayoutParams.rightMargin, uploadProgressLayoutParams.bottomMargin);
                uploadProgressLayout.setLayoutParams(uploadProgressLayoutParams);
            }
        }
        return isMergedView;
    }

    public int presenceOnlineColor() {
        return ContextCompat.getColor(mContext, R.color.presence_online);
    }

    public int presenceOfflineColor() {
        return ContextCompat.getColor(mContext, R.color.presence_offline);
    }

    public int presenceUnavailableColor() {
        return ContextCompat.getColor(mContext, R.color.presence_unavailable);
    }

    @Override
    public int getHighlightMessageTextColor(Context context) {
        return ContextCompat.getColor(mContext, R.color.vector_fuchsia_color);
    }

    @Override
    public int getSearchHighlightMessageTextColor(Context context) {
        return ContextCompat.getColor(mContext, R.color.vector_green_color);
    }

    @Override
    public int getNotSentMessageTextColor(Context context) {
        return ContextCompat.getColor(mContext, R.color.vector_not_send_color);
    }

    //==============================================================================================================
    // Download / upload progress management
    //==============================================================================================================

    /**
     * Format a second time range.
     * @param seconds the seconds time
     * @return the formatted string
     */
    private static String vectorRemainingTimeToString(Context context, int seconds) {
        if (seconds < 0) {
            return "";
        } else if (seconds <= 1) {
            return "< 1s";
        } else if (seconds < 60) {
            return context.getString(R.string.attachment_remaining_time_seconds, seconds);
        } else if (seconds < 3600) {
            return context.getString(R.string.attachment_remaining_time_minutes, (seconds / 60), (seconds % 60));
        } else {
            return DateUtils.formatElapsedTime(seconds);
        }
    }

    /**
     * Format the download / upload stats.
     * @param context the context.
     * @param progressFileSize the upload / download media size.
     * @param fileSize the expected media size.
     * @param remainingTime the remaining time (seconds)
     * @return the formatted string.
     */
    private static String formatStats(Context context, int progressFileSize, int fileSize, int remainingTime) {
        String formattedString = "";

        if (fileSize > 0) {
            formattedString += android.text.format.Formatter.formatShortFileSize(context, progressFileSize);
            formattedString += " / " + android.text.format.Formatter.formatShortFileSize(context, fileSize);
        }

        if (remainingTime > 0) {
            if (!TextUtils.isEmpty(formattedString)) {
                formattedString += " (" + vectorRemainingTimeToString(context, remainingTime) + ")";
            } else {
                formattedString += vectorRemainingTimeToString(context, remainingTime);
            }
        }

        return formattedString;
    }

    /**
     * Format the download stats.
     * @param context the context.
     * @param stats the download stats
     * @return the formatted string
     */
    private static String formatDownloadStats(Context context, IMXMediaDownloadListener.DownloadStats stats) {
        return formatStats(context, stats.mDownloadedSize, stats.mFileSize, stats.mEstimatedRemainingTime);
    }

    /**
     * Format the upload stats.
     * @param context the context.
     * @param stats the upload stats
     * @return the formatted string
     */
    private static String formatUploadStats(Context context, IMXMediaUploadListener.UploadStats stats) {
        return formatStats(context, stats.mUploadedSize, stats.mFileSize, stats.mEstimatedRemainingTime);
    }

    //
    private final HashMap<String, String> mMediaDownloadIdByEventId = new HashMap<>();

    /**
     * Tells if the downloadId is the media download id.
     * @param event the event
     * @param downloadId the download id.
     * @return true if the media is downloading (not the thumbnail)
     */
    private boolean isMediaDownloading(Event event, String downloadId) {
        String mediaDownloadId = mMediaDownloadIdByEventId.get(event.eventId);

        if (null == mediaDownloadId) {
            mediaDownloadId = "";

            if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE)) {
                Message message = JsonUtils.toMessage(event.getContent());

                String url = null;

                if (TextUtils.equals(message.msgtype, Message.MSGTYPE_IMAGE)) {
                    url = JsonUtils.toImageMessage(event.getContent()).getUrl();
                } else if (TextUtils.equals(message.msgtype, Message.MSGTYPE_VIDEO)) {
                    url = JsonUtils.toVideoMessage(event.getContent()).getUrl();
                } else if (TextUtils.equals(message.msgtype, Message.MSGTYPE_FILE)) {
                    url = JsonUtils.toFileMessage(event.getContent()).getUrl();
                }

                if (!TextUtils.isEmpty(url)) {
                    mediaDownloadId = mSession.getMediasCache().downloadIdFromUrl(url);
                }
            }

            mMediaDownloadIdByEventId.put(event.eventId, mediaDownloadId);
        }

        return TextUtils.equals(mediaDownloadId, downloadId);
    }

    @Override
    protected void refreshDownloadViews(final Event event,
            final IMXMediaDownloadListener.DownloadStats downloadStats, final View downloadProgressLayout) {
        if ((null != downloadStats) && isMediaDownloading(event, downloadStats.mDownloadId)) {
            downloadProgressLayout.setVisibility(View.VISIBLE);

            TextView downloadProgressStatsTextView = (TextView) downloadProgressLayout
                    .findViewById(R.id.media_progress_text_view);
            ProgressBar progressBar = (ProgressBar) downloadProgressLayout.findViewById(R.id.media_progress_view);

            if (null != downloadProgressStatsTextView) {
                downloadProgressStatsTextView.setText(formatDownloadStats(mContext, downloadStats));
            }

            if (null != progressBar) {
                progressBar.setProgress(downloadStats.mProgress);
            }

            final View cancelLayout = downloadProgressLayout.findViewById(R.id.media_progress_cancel);

            if (null != cancelLayout) {
                cancelLayout.setTag(event);

                cancelLayout.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (event == cancelLayout.getTag()) {
                            if (null != mVectorMessagesAdapterEventsListener) {
                                mVectorMessagesAdapterEventsListener.onEventAction(event, "",
                                        R.id.ic_action_vector_cancel_download);
                            }
                        }
                    }
                });
            }
        } else {
            downloadProgressLayout.setVisibility(View.GONE);
        }
    }

    @Override
    protected void updateUploadProgress(View uploadProgressLayout, int progress) {
        ProgressBar progressBar = (ProgressBar) uploadProgressLayout.findViewById(R.id.media_progress_view);

        if (null != progressBar) {
            progressBar.setProgress(progress);
        }
    }

    @Override
    protected void refreshUploadViews(final Event event, final IMXMediaUploadListener.UploadStats uploadStats,
            final View uploadProgressLayout) {
        if (null != uploadStats) {
            uploadProgressLayout.setVisibility(View.VISIBLE);

            TextView uploadProgressStatsTextView = (TextView) uploadProgressLayout
                    .findViewById(R.id.media_progress_text_view);
            ProgressBar progressBar = (ProgressBar) uploadProgressLayout.findViewById(R.id.media_progress_view);

            if (null != uploadProgressStatsTextView) {
                uploadProgressStatsTextView.setText(formatUploadStats(mContext, uploadStats));
            }

            if (null != progressBar) {
                progressBar.setProgress(uploadStats.mProgress);
            }

            final View cancelLayout = uploadProgressLayout.findViewById(R.id.media_progress_cancel);

            if (null != cancelLayout) {
                cancelLayout.setTag(event);

                cancelLayout.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (event == cancelLayout.getTag()) {
                            if (null != mVectorMessagesAdapterEventsListener) {
                                mVectorMessagesAdapterEventsListener.onEventAction(event, "",
                                        R.id.ic_action_vector_cancel_upload);
                            }
                        }
                    }
                });
            }
        } else {
            uploadProgressLayout.setVisibility(View.GONE);
        }
    }
}