public class CustomMessageAdapter extends QBMessagesAdapter<QBChatMessage> {
    private static final String TAG = CustomMessageAdapter.class.getSimpleName();
    protected static final int TYPE_OWN_VIDEO_ATTACH = 5;
    protected static final int TYPE_OPPONENT_VIDEO_ATTACH = 6;

    private QBUser currentUser;
    private QBUser opponentUser;
    private UserData currentUserData;
    private UserData opponentUserData;

    public CustomMessageAdapter(Activity context, List<QBChatMessage> chatMessages, ArrayList<QBUser> qbUsers) {
        super(context, chatMessages);
        setUsers(qbUsers);
    }

    private void setUsers(ArrayList<QBUser> qbUsers) {
        int currentUserID = QBChatService.getInstance().getUser().getId();

        for (QBUser user : qbUsers) {
            if (user.getId().equals(currentUserID)) {
                currentUser = user;
            } else {
                opponentUser = user;
            }
        }
        setUsersData();
    }

    private void setUsersData() {
        currentUserData = new Gson().fromJson(currentUser.getCustomData(), UserData.class);
        opponentUserData = new Gson().fromJson(opponentUser.getCustomData(), UserData.class);
    }

    @Override
    protected int customViewType(int position) {
        QBChatMessage chatMessage = getItem(position);

        if (hasAttachments(chatMessage)) {
            QBAttachment attachment = chatMessage.getAttachments().iterator().next();
            if (QBAttachment.VIDEO_TYPE.equals(attachment.getType())) {
                return isIncoming(chatMessage) ? TYPE_OPPONENT_VIDEO_ATTACH : TYPE_OWN_VIDEO_ATTACH;
            }
        }
        return -1;
    }

    @Override
    protected void onBindViewCustomHolder(QBMessageViewHolder holder, QBChatMessage chatMessage, int position) {
        displayAttachment(holder, position);
        holder.avatar.setVisibility(View.GONE);
    }

    @Override
    protected void onBindViewMsgRightHolder(TextMessageHolder holder, QBChatMessage chatMessage, int position) {
        TextView view = (TextView) holder.itemView.findViewById(R.id.custom_text_view);
        view.setText(currentUser.getFullName());
        super.onBindViewMsgRightHolder(holder, chatMessage, position);
    }

    @Override
    protected void onBindViewMsgLeftHolder(TextMessageHolder holder, QBChatMessage chatMessage, int position) {
        holder.timeTextMessageTextView.setVisibility(View.GONE);

        TextView textView = (TextView) holder.itemView.findViewById(R.id.opponent_name_text_view);
        textView.setText(opponentUser.getFullName());

        TextView customMessageTimeTextView = (TextView) holder.itemView.findViewById(R.id.custom_msg_text_time_message);
        customMessageTimeTextView.setText(getDate(chatMessage.getDateSent()));

        super.onBindViewMsgLeftHolder(holder, chatMessage, position);
    }

    @Override
    public String getImageUrl(int position) {
        QBAttachment attachment = getQBAttach(position);
        return attachment.getUrl();
    }

    @Override
    public String obtainAvatarUrl(int valueType, QBChatMessage chatMessage) {
        return currentUser.getId().equals(chatMessage.getSenderId()) ?
                currentUserData.getUserAvatar() : opponentUserData.getUserAvatar();
    }
} public class ChatHelper {
    private static final String TAG = ChatHelper.class.getSimpleName();

    private static final int CHAT_SOCKET_TIMEOUT = 0;

    private static final int CHAT_HISTORY_ITEMS_PER_PAGE = 100;
    private static final String CHAT_HISTORY_ITEMS_SORT_FIELD = "date_sent";

    private static ChatHelper instance;

    private QBChatService qbChatService;

    public static synchronized ChatHelper getInstance() {
        if (instance == null) {
            QBSettings.getInstance().setLogLevel(LogLevel.DEBUG);
            QBChatService.setDebugEnabled(true);
            QBChatService.setConfigurationBuilder(buildChatConfigs());
            QBChatService.setDefaultPacketReplyTimeout(20 * 1000);
            instance = new ChatHelper();
        }
        return instance;
    }

    private ChatHelper() {
        qbChatService = QBChatService.getInstance();
    }

    private static QBChatService.ConfigurationBuilder buildChatConfigs() {
        QBChatService.ConfigurationBuilder configurationBuilder = new QBChatService.ConfigurationBuilder();
        configurationBuilder.setKeepAlive(true)
                .setSocketTimeout(CHAT_SOCKET_TIMEOUT)
                .setAutojoinEnabled(false);

        return configurationBuilder;
    }

    public void loginAndGetUsers(final QBUser user, final QBEntityCallback<ArrayList<QBUser>> callback) {
        Performer<QBSession> performer = QBAuth.createSession(user);
        final Observable<QBSession> observableSession = performer.convertTo(RxJavaPerformProcessor.INSTANCE);

        Observable<Void> observableLoginToChat = Observable.fromCallable(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                QBChatService.getInstance().login(user);
                return null;
            }
        });

        performMultiRequest(observableSession, observableLoginToChat, callback);
    }

    private void performMultiRequest(Observable<QBSession> session, Observable<Void> loginToChat, final QBEntityCallback<ArrayList<QBUser>> callback) {
        final ArrayList<Integer> usersIds = new ArrayList<>();
        usersIds.add(Consts.userOneID);
        usersIds.add(Consts.userTwoID);

        Observable.zip(
                loginToChat.subscribeOn(Schedulers.io()),
                session.flatMap(new Func1<QBSession, Observable<ArrayList<QBUser>>>() {
                    @Override
                    public Observable<ArrayList<QBUser>> call(QBSession qbSession) {
                        return QBUsers.getUsersByIDs(usersIds, null).convertTo(RxJavaPerformProcessor.INSTANCE);
                    }
                })
                        .subscribeOn(Schedulers.io()),
                mergeResult()
        )
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<ArrayList<QBUser>>() {
                    @Override
                    public void onCompleted() {
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "onError " + e);
                    }

                    @Override
                    public void onNext(ArrayList<QBUser> qbUsers) {
                        Log.d(TAG, "onNext" + qbUsers);
                        callback.onSuccess(qbUsers, Bundle.EMPTY);
                    }
                });
    }

    private Func2<Void, ArrayList<QBUser>, ArrayList<QBUser>> mergeResult() {
        return new Func2<Void, ArrayList<QBUser>, ArrayList<QBUser>>() {
            @Override
            public ArrayList<QBUser> call(Void aVoid, ArrayList<QBUser> qbUsers) {
                return qbUsers;
            }
        };
    }

    public void loadChatHistory(QBChatDialog dialog, int skipPagination,
                                final QBEntityCallback<ArrayList<QBChatMessage>> callback) {
        QBRequestGetBuilder customObjectRequestBuilder = new QBRequestGetBuilder();
        customObjectRequestBuilder.setSkip(skipPagination);
        customObjectRequestBuilder.setLimit(CHAT_HISTORY_ITEMS_PER_PAGE);
        customObjectRequestBuilder.sortDesc(CHAT_HISTORY_ITEMS_SORT_FIELD);

        QBRestChatService.getDialogMessages(dialog, customObjectRequestBuilder).performAsync(
                new QBEntityCallback<ArrayList<QBChatMessage>>() {
                    @Override
                    public void onSuccess(ArrayList<QBChatMessage> qbChatMessages, Bundle bundle) {
                        callback.onSuccess(qbChatMessages, bundle);
                    }

                    @Override
                    public void onError(QBResponseException e) {
                        callback.onError(e);
                    }
                });
    }

    public void logout() {
        qbChatService.destroy();
    }
} public class QBMessagesAdapter<T extends QBChatMessage> extends RecyclerView.Adapter<QBMessagesAdapter.QBMessageViewHolder> implements QBBaseAdapter<T> {
    private static final String TAG = QBMessagesAdapter.class.getSimpleName();

    protected static final long SHOW_VIEW_ANIMATION_DURATION = 500;

    protected static final int TYPE_TEXT_RIGHT = 1;
    protected static final int TYPE_TEXT_LEFT = 2;
    protected static final int TYPE_ATTACH_RIGHT = 3;
    protected static final int TYPE_ATTACH_LEFT = 4;
    protected static final int TYPE_ATTACH_RIGHT_AUDIO = 5;
    protected static final int TYPE_ATTACH_LEFT_AUDIO = 6;
    protected static final int TYPE_ATTACH_RIGHT_VIDEO = 7;
    protected static final int TYPE_ATTACH_LEFT_VIDEO = 8;

    //Message TextView click listener
    //
    private QBChatMessageLinkClickListener messageTextViewLinkClickListener;
    private boolean overrideOnClick;

    private QBChatAttachClickListener attachImageClickListener;
    private QBChatAttachClickListener attachLocationClickListener;
    private QBChatAttachClickListener attachAudioClickListener;
    private QBChatAttachClickListener attachVideoClickListener;

    private QBLinkPreviewClickListener linkPreviewClickListener;
    private boolean overrideOnLinkPreviewClick;


    private SparseIntArray containerLayoutRes = new SparseIntArray() {
        {
            put(TYPE_TEXT_RIGHT, R.layout.list_item_text_right);
            put(TYPE_TEXT_LEFT, R.layout.list_item_text_left);
            put(TYPE_ATTACH_RIGHT, R.layout.list_item_attach_right);
            put(TYPE_ATTACH_LEFT, R.layout.list_item_attach_left);
            put(TYPE_ATTACH_RIGHT_AUDIO, R.layout.list_item_attach_right_audio);
            put(TYPE_ATTACH_LEFT_AUDIO, R.layout.list_item_attach_left_audio);
            put(TYPE_ATTACH_RIGHT_VIDEO, R.layout.list_item_attach_right_video);
            put(TYPE_ATTACH_LEFT_VIDEO, R.layout.list_item_attach_left_video);
        }
    };

    protected QBMessageViewHolder qbViewHolder;

    protected List<T> chatMessages;
    protected LayoutInflater inflater;
    protected Context context;

    private SingleMediaManager mediaManager;
    private AudioController audioController;
    private MediaControllerEventListener mediaControllerEventListener;
    private Map<QBPlaybackControlView, Integer> playerViewHashMap;
    private int activePlayerViewPosition = -1;


    public QBMessagesAdapter(Context context, List<T> chatMessages) {
        this.context = context;
        this.chatMessages = chatMessages;
        this.inflater = LayoutInflater.from(context);
    }

    public QBChatMessageLinkClickListener getMessageTextViewLinkClickListener() {
        return messageTextViewLinkClickListener;
    }

    /**
     * Sets listener for handling pressed links on message text.
     *
     * @param textViewLinkClickListener listener to set. Must to implement {@link QBChatMessageLinkClickListener}
     * @param overrideOnClick           set 'true' if have to himself manage onLinkClick event or set 'false' for delegate
     *                                  onLinkClick event to {@link android.text.util.Linkify}
     */
    public void setMessageTextViewLinkClickListener(QBChatMessageLinkClickListener textViewLinkClickListener, boolean overrideOnClick) {
        this.messageTextViewLinkClickListener = textViewLinkClickListener;
        this.overrideOnClick = overrideOnClick;
    }

    public void setAttachImageClickListener(QBChatAttachClickListener clickListener) {
        attachImageClickListener = clickListener;
    }

    public void setAttachLocationClickListener(QBChatAttachClickListener clickListener) {
        attachLocationClickListener = clickListener;
    }

    public void setLinkPreviewClickListener(QBLinkPreviewClickListener linkPreviewClickListener, boolean overrideOnLinkpreviewClick) {
        this.linkPreviewClickListener = linkPreviewClickListener;
        this.overrideOnLinkPreviewClick = overrideOnLinkpreviewClick;
    }

    public void setAttachAudioClickListener(QBChatAttachClickListener clickListener) {
        this.attachAudioClickListener = clickListener;
    }

    public void setAttachVideoClickListener(QBChatAttachClickListener clickListener) {
        this.attachVideoClickListener = clickListener;
    }

    public void removeAttachImageClickListener(QBChatAttachClickListener clickListener) {
        attachImageClickListener = null;
    }

    public void removeLocationImageClickListener(QBChatAttachClickListener clickListener) {
        attachLocationClickListener = null;
    }

    public void removeAttachAudioClickListener(QBChatAttachClickListener clickListener) {
        attachAudioClickListener = null;
    }

    public void removeAttachVideoClickListener(QBChatAttachClickListener clickListener) {
        attachVideoClickListener = null;
    }

    /**
     * Removes listener for handling onLinkClick event on message text.
     */
    public void removeMessageTextViewLinkClickListener() {
        this.messageTextViewLinkClickListener = null;
        this.overrideOnClick = false;
    }

    public void setMediaPlayerListener(QBMediaPlayerListener mediaPlayerListener) {
        getMediaManagerInstance().addListener(mediaPlayerListener);
    }

    public void removeMediaPlayerListener(QBMediaPlayerListener listener) {
        getMediaManagerInstance().removeListener(listener);
    }

    public void removeLinkPreviewClickListener() {
        this.linkPreviewClickListener = null;
        this.overrideOnLinkPreviewClick = false;
    }

    @Override
    public QBMessageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        switch (viewType) {
            case TYPE_TEXT_RIGHT:
                qbViewHolder = new TextMessageHolder(inflater.inflate(containerLayoutRes.get(viewType), parent, false), R.id.msg_text_message,
                        R.id.msg_text_time_message, R.id.msg_link_preview);
                return qbViewHolder;
            case TYPE_TEXT_LEFT:
                qbViewHolder = new TextMessageHolder(inflater.inflate(containerLayoutRes.get(viewType), parent, false), R.id.msg_text_message,
                        R.id.msg_text_time_message, R.id.msg_link_preview);
                return qbViewHolder;
            case TYPE_ATTACH_RIGHT:
                qbViewHolder = new ImageAttachHolder(inflater.inflate(containerLayoutRes.get(viewType), parent, false), R.id.msg_image_attach, R.id.msg_progressbar_attach,
                        R.id.msg_text_time_attach, R.id.msg_signs_attach);
                return qbViewHolder;
            case TYPE_ATTACH_LEFT:
                qbViewHolder = new ImageAttachHolder(inflater.inflate(containerLayoutRes.get(viewType), parent, false), R.id.msg_image_attach, R.id.msg_progressbar_attach,
                        R.id.msg_text_time_attach, R.id.msg_signs_attach);
                return qbViewHolder;
            case TYPE_ATTACH_RIGHT_AUDIO:
                qbViewHolder = new AudioAttachHolder(inflater.inflate(containerLayoutRes.get(viewType), parent, false), R.id.msg_audio_attach, R.id.msg_attach_duration,
                        R.id.msg_text_time_attach, R.id.msg_signs_attach);
                return qbViewHolder;
            case TYPE_ATTACH_LEFT_AUDIO:
                qbViewHolder = new AudioAttachHolder(inflater.inflate(containerLayoutRes.get(viewType), parent, false), R.id.msg_audio_attach, R.id.msg_attach_duration,
                        R.id.msg_text_time_attach, R.id.msg_signs_attach);
                return qbViewHolder;
            case TYPE_ATTACH_RIGHT_VIDEO:
                qbViewHolder = new VideoAttachHolder(inflater.inflate(containerLayoutRes.get(viewType), parent, false), R.id.msg_video_attach, R.id.msg_progressbar_attach,
                        R.id.msg_attach_duration, R.id.msg_text_time_attach, R.id.msg_signs_attach, R.id.msg_video_play_icon);
                return qbViewHolder;
            case TYPE_ATTACH_LEFT_VIDEO:
                qbViewHolder = new VideoAttachHolder(inflater.inflate(containerLayoutRes.get(viewType), parent, false), R.id.msg_video_attach, R.id.msg_progressbar_attach,
                        R.id.msg_attach_duration, R.id.msg_text_time_attach, R.id.msg_signs_attach, R.id.msg_video_play_icon);
                return qbViewHolder;

            default:
                Log.d(TAG, "onCreateViewHolder case default");
                // resource must be set manually by creating custom adapter
                return onCreateCustomViewHolder(parent, viewType);
        }
    }

    @Override
    public void onViewRecycled(QBMessageViewHolder holder) {
        if (holder.getItemViewType() == TYPE_TEXT_LEFT || holder.getItemViewType() == TYPE_TEXT_RIGHT) {
            TextMessageHolder textMessageHolder = (TextMessageHolder) holder;

            if (textMessageHolder.linkPreviewLayout.getTag() != null) {
                textMessageHolder.linkPreviewLayout.setTag(null);
            }
        }

        //abort loading avatar before setting new avatar to view
        if (containerLayoutRes.get(holder.getItemViewType()) != 0) {
            Glide.clear(holder.avatar);
        }

        super.onViewRecycled(holder);
    }

    protected QBMessageViewHolder onCreateCustomViewHolder(ViewGroup parent, int viewType) {
        Log.e(TAG, "You must create ViewHolder by your own");
        return null;
    }

    protected void setMsgLayoutResourceByType(int typeLayout, @LayoutRes int messageLayoutResource) {
        containerLayoutRes.put(typeLayout, messageLayoutResource);
    }

    @Override
    public void onBindViewHolder(QBMessageViewHolder holder, int position) {
        T chatMessage = getItem(position);
        int valueType = getItemViewType(position);
        switch (valueType) {
            case TYPE_TEXT_RIGHT:
                onBindViewMsgRightHolder((TextMessageHolder) holder, chatMessage, position);
                break;
            case TYPE_TEXT_LEFT:
                onBindViewMsgLeftHolder((TextMessageHolder) holder, chatMessage, position);
                break;
            case TYPE_ATTACH_RIGHT:
                Log.i(TAG, "onBindViewHolder TYPE_ATTACH_RIGHT");
                onBindViewAttachRightHolder((ImageAttachHolder) holder, chatMessage, position);
                break;
            case TYPE_ATTACH_LEFT:
                Log.i(TAG, "onBindViewHolder TYPE_ATTACH_LEFT");
                onBindViewAttachLeftHolder((ImageAttachHolder) holder, chatMessage, position);
                break;
            case TYPE_ATTACH_RIGHT_AUDIO:
                Log.i(TAG, "onBindViewHolder TYPE_ATTACH_RIGHT_AUDIO");
                onBindViewAttachRightAudioHolder((AudioAttachHolder) holder, chatMessage, position);
                break;
            case TYPE_ATTACH_LEFT_AUDIO:
                Log.i(TAG, "onBindViewHolder TYPE_ATTACH_LEFT_AUDIO");
                onBindViewAttachLeftAudioHolder((AudioAttachHolder) holder, chatMessage, position);
                break;
            case TYPE_ATTACH_RIGHT_VIDEO:
                Log.i(TAG, "onBindViewHolder TYPE_ATTACH_RIGHT_VIDEO");
                onBindViewAttachRightVideoHolder((VideoAttachHolder) holder, chatMessage, position);
                break;
            case TYPE_ATTACH_LEFT_VIDEO:
                Log.i(TAG, "onBindViewHolder TYPE_ATTACH_LEFT_VIDEO");
                onBindViewAttachLeftVideoHolder((VideoAttachHolder) holder, chatMessage, position);
                break;
            default:
                onBindViewCustomHolder(holder, chatMessage, position);
                Log.i(TAG, "onBindViewHolder TYPE_ATTACHMENT_CUSTOM");
                break;
        }
    }

    protected void onBindViewCustomHolder(QBMessageViewHolder holder, T chatMessage, int position) {
    }

    protected void onBindViewAttachRightHolder(ImageAttachHolder holder, T chatMessage, int position) {
        setDateSentAttach(holder, chatMessage);
        displayAttachment(holder, position);

        int valueType = getItemViewType(position);
        String avatarUrl = obtainAvatarUrl(valueType, chatMessage);
        if (avatarUrl != null) {
            displayAvatarImage(avatarUrl, holder.avatar);
        }

        setItemAttachClickListener(getAttachListenerByType(position), holder, getQBAttach(position), position);
    }

    protected void onBindViewAttachLeftHolder(ImageAttachHolder holder, T chatMessage, int position) {
        setDateSentAttach(holder, chatMessage);
        displayAttachment(holder, position);

        int valueType = getItemViewType(position);
        String avatarUrl = obtainAvatarUrl(valueType, chatMessage);
        if (avatarUrl != null) {
            displayAvatarImage(avatarUrl, holder.avatar);
        }

        setItemAttachClickListener(getAttachListenerByType(position), holder, getQBAttach(position), position);
    }

    protected void onBindViewAttachRightAudioHolder(AudioAttachHolder holder, T chatMessage, int position) {
        setDateSentAttach(holder, chatMessage);
        displayAttachmentAudio(holder, position);

        int valueType = getItemViewType(position);
        String avatarUrl = obtainAvatarUrl(valueType, chatMessage);
        if (avatarUrl != null) {
            displayAvatarImage(avatarUrl, holder.avatar);
        }
    }


    protected void onBindViewAttachLeftAudioHolder(AudioAttachHolder holder, T chatMessage, int position) {
        setDateSentAttach(holder, chatMessage);
        displayAttachmentAudio(holder, position);

        int valueType = getItemViewType(position);
        String avatarUrl = obtainAvatarUrl(valueType, chatMessage);
        if (avatarUrl != null) {
            displayAvatarImage(avatarUrl, holder.avatar);
        }
    }

    protected void onBindViewAttachRightVideoHolder(VideoAttachHolder holder, T chatMessage, int position) {
        setDateSentAttach(holder, chatMessage);
        displayAttachmentVideo(holder, position);

        int valueType = getItemViewType(position);
        String avatarUrl = obtainAvatarUrl(valueType, chatMessage);
        if (avatarUrl != null) {
            displayAvatarImage(avatarUrl, holder.avatar);
        }

        setItemAttachClickListener(getAttachListenerByType(position), holder, getQBAttach(position), position);
    }

    protected void onBindViewAttachLeftVideoHolder(VideoAttachHolder holder, T chatMessage, int position) {
        setDateSentAttach(holder, chatMessage);
        displayAttachmentVideo(holder, position);

        int valueType = getItemViewType(position);
        String avatarUrl = obtainAvatarUrl(valueType, chatMessage);
        if (avatarUrl != null) {
            displayAvatarImage(avatarUrl, holder.avatar);
        }

        setItemAttachClickListener(getAttachListenerByType(position), holder, getQBAttach(position), position);
    }

    protected void onBindViewMsgLeftHolder(TextMessageHolder holder, T chatMessage, int position) {
        fillTextMessageHolder(holder, chatMessage, position, true);
    }

    protected void onBindViewMsgRightHolder(TextMessageHolder holder, T chatMessage, int position) {
        fillTextMessageHolder(holder, chatMessage, position, false);
    }

    protected void fillTextMessageHolder(TextMessageHolder holder, T chatMessage, int position, boolean isLeftMessage) {
        holder.linkPreviewLayout.setVisibility(View.GONE);
        holder.messageTextView.setText(chatMessage.getBody());
        holder.timeTextMessageTextView.setText(getDate(chatMessage.getDateSent()));

        setMessageTextViewLinkClickListener(holder, position);

        int valueType = getItemViewType(position);
        String avatarUrl = obtainAvatarUrl(valueType, chatMessage);
        if (avatarUrl != null) {
            displayAvatarImage(avatarUrl, holder.avatar);
        }

        final List<String> urlsList = LinkUtils.extractUrls(chatMessage.getBody());
        if (!urlsList.isEmpty()) {
            holder.messageTextView.setMaxWidth((int) context.getResources().getDimension(R.dimen.link_preview_width));
            holder.linkPreviewLayout.setTag(chatMessage.getId());

            if (isLeftMessage) {
                processLinksFromLeftMessage(holder, urlsList, position);
            } else {
                processLinksFromRightMessage(holder, urlsList, position);
            }
        } else {
            holder.messageTextView.setMaxWidth(context.getResources().getDisplayMetrics().widthPixels);
        }
    }

    protected void processLinksFromLeftMessage(TextMessageHolder holder, List<String> urlsList, int position) {
        processLinksFromMessage(holder, urlsList, position);
    }

    protected void processLinksFromRightMessage(TextMessageHolder holder, List<String> urlsList, int position) {
        processLinksFromMessage(holder, urlsList, position);
    }

    protected void processLinksFromMessage(TextMessageHolder holder, final List<String> urlsList, final int position) {
        final String firstLink = LinkUtils.getLinkWithProtocol(urlsList.get(0));
        String linkPreviewViewIdentifier = (String) holder.linkPreviewLayout.getTag();

        QBLinkPreviewCashService.getInstance().getLinkPreview(firstLink,
                null,
                false,
                new LoadLinkPreviewHandler(holder, urlsList, position, linkPreviewViewIdentifier));
    }

    protected void fillLinkPreviewLayout(final View linkPreviewLayout, final QBLinkPreview linkPreview, String link) {
        TextView linkTitle = (TextView) linkPreviewLayout.findViewById(R.id.link_preview_title);
        TextView linkDescription = (TextView) linkPreviewLayout.findViewById(R.id.link_preview_description);
        ImageView linkImage = (ImageView) linkPreviewLayout.findViewById(R.id.link_preview_image);
        ImageView linkHostIcon = (ImageView) linkPreviewLayout.findViewById(R.id.link_host_icon);
        TextView linkHost = (TextView) linkPreviewLayout.findViewById(R.id.link_host_url);

        linkTitle.setText(linkPreview.getTitle());

        if (!TextUtils.isEmpty(linkPreview.getDescription())) {
            linkDescription.setText(linkPreview.getDescription());
            linkDescription.setVisibility(View.VISIBLE);
        } else {
            linkDescription.setVisibility(View.GONE);
        }

        linkHost.setText(LinkUtils.getHostFromLink(link));

        linkImage.setVisibility(View.GONE);
        if (linkPreview.getImage() != null && linkPreview.getImage().getImageUrl() != null && LoadImagesUtils.isPossibleToDisplayImage(context)) {
            loadImageOrHideView(LinkUtils.prepareCorrectLink(linkPreview.getImage().getImageUrl()), linkImage);
        }

        linkHostIcon.setVisibility(View.GONE);
        if (LinkUtils.getLinkForHostIcon(link) != null && LoadImagesUtils.isPossibleToDisplayImage(context)) {
            loadImageOrHideView(LinkUtils.getLinkForHostIcon(link), linkHostIcon);
        }
    }

    protected void loadImageOrHideView(final String imageUrl, final ImageView imageView) {
        int preferredImageWidth = (int) context.getResources().getDimension(R.dimen.attach_image_width_preview);
        int preferredImageHeight = (int) context.getResources().getDimension(R.dimen.attach_image_height_preview);

        Glide.with(context)
                .load(imageUrl)
                .listener(new RequestListener<String, GlideDrawable>() {
                    @Override
                    public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
                        return false;
                    }

                    @Override
                    public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
                        AnimationsUtils.showView(imageView, SHOW_VIEW_ANIMATION_DURATION);
                        return false;
                    }
                })
                .override(preferredImageWidth, preferredImageHeight)
                .dontTransform()
                .into(imageView);
    }

    private void setMessageTextViewLinkClickListener(TextMessageHolder holder, int position) {
        if (messageTextViewLinkClickListener != null) {
            QBMessageTextClickMovement customClickMovement =
                    new QBMessageTextClickMovement(messageTextViewLinkClickListener, overrideOnClick, context);
            customClickMovement.setPositionInAdapter(position);

            holder.messageTextView.setMovementMethod(customClickMovement);
        }
    }

    private QBChatAttachClickListener getAttachListenerByType(int position) {
        QBAttachment attachment = getQBAttach(position);

        if (QBAttachment.PHOTO_TYPE.equalsIgnoreCase(attachment.getType()) ||
                QBAttachment.IMAGE_TYPE.equalsIgnoreCase(attachment.getType())) {
            return attachImageClickListener;
        } else if (QBAttachment.LOCATION_TYPE.equalsIgnoreCase(attachment.getType())) {
            return attachLocationClickListener;
        } else if (QBAttachment.AUDIO_TYPE.equalsIgnoreCase(attachment.getType())) {
            return attachAudioClickListener;
        } else if (QBAttachment.VIDEO_TYPE.equalsIgnoreCase(attachment.getType())) {
            return attachVideoClickListener;
        }
        return null;
    }

    protected void setDateSentAttach(BaseAttachHolder holder, T chatMessage) {
        holder.attachTextTime.setText(getDate(chatMessage.getDateSent()));
    }

    /**
     * ObtainAvatarUrl must be implemented in derived class
     *
     * @return String avatar url
     */
    @Nullable
    public String obtainAvatarUrl(int valueType, T chatMessage) {
        return null;
    }

    @Override
    public int getItemCount() {
        return chatMessages.size();
    }

    @Override
    public T getItem(int position) {
        return chatMessages.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public int getItemViewType(int position) {
        T chatMessage = getItem(position);

        if (hasAttachments(chatMessage)) {
            QBAttachment attachment = getQBAttach(position);

            if (QBAttachment.PHOTO_TYPE.equalsIgnoreCase(attachment.getType()) ||
                    QBAttachment.IMAGE_TYPE.equalsIgnoreCase(attachment.getType())) {
                return isIncoming(chatMessage) ? TYPE_ATTACH_LEFT : TYPE_ATTACH_RIGHT;
            } else if (QBAttachment.LOCATION_TYPE.equalsIgnoreCase(attachment.getType())) {
                return getLocationView(chatMessage);
            } else if (QBAttachment.AUDIO_TYPE.equalsIgnoreCase(attachment.getType())) {
                return isIncoming(chatMessage) ? TYPE_ATTACH_LEFT_AUDIO : TYPE_ATTACH_RIGHT_AUDIO;
            } else if (QBAttachment.VIDEO_TYPE.equalsIgnoreCase(attachment.getType())) {
                return isIncoming(chatMessage) ? TYPE_ATTACH_LEFT_VIDEO : TYPE_ATTACH_RIGHT_VIDEO;
            }

        } else {
            return isIncoming(chatMessage) ? TYPE_TEXT_LEFT : TYPE_TEXT_RIGHT;
        }
        return customViewType(position);
    }

    protected int getLocationView(T chatMessage) {
        return isIncoming(chatMessage) ? TYPE_ATTACH_LEFT : TYPE_ATTACH_RIGHT;
    }

    protected int customViewType(int position) {
        return -1;
    }

    @Override
    public void add(T item) {
        chatMessages.add(item);
        notifyDataSetChanged();
    }

    @Override
    public List<T> getList() {
        return chatMessages;
    }

    @Override
    public void addList(List<T> items) {
        chatMessages.clear();
        chatMessages.addAll(items);
        notifyDataSetChanged();
    }

    protected boolean isIncoming(T chatMessage) {
        QBUser currentUser = QBChatService.getInstance().getUser();
        return chatMessage.getSenderId() != null && !chatMessage.getSenderId().equals(currentUser.getId());
    }

    protected boolean hasAttachments(T chatMessage) {
        Collection<QBAttachment> attachments = chatMessage.getAttachments();
        return attachments != null && !attachments.isEmpty();
    }

    /**
     * @return string in "Hours:Minutes" format, i.e. <b>10:15</b>
     */
    protected String getDate(long seconds) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
        return dateFormat.format(new Date(seconds * 1000));
    }

    /**
     * displayAttachment must be implemented in derived class
     */
    protected void displayAttachment(QBMessageViewHolder holder, int position) {
        QBAttachment attachment = getQBAttach(position);

        if (QBAttachment.PHOTO_TYPE.equalsIgnoreCase(attachment.getType()) ||
                QBAttachment.IMAGE_TYPE.equalsIgnoreCase(attachment.getType())) {
            showPhotoAttach(holder, position);
        } else if (QBAttachment.LOCATION_TYPE.equalsIgnoreCase(attachment.getType())) {
            showLocationAttach(holder, position);
        }
    }

    protected void displayAttachmentAudio(QBMessageViewHolder holder, int position) {
        QBAttachment attachment = getQBAttach(position);

        Uri uri = getUriFromAttach(attachment);
        int duration = getDurationFromAttach(attachment, position);
        setDurationAudio(duration, holder);
        QBPlaybackControlView playerView = ((AudioAttachHolder) holder).playerView;

        showAudioView(playerView, uri, position);
        setItemAttachAudioClickListener(getAttachListenerByType(position), holder, getQBAttach(position), position, playerView);
    }


    protected void displayAttachmentVideo(QBMessageViewHolder holder, int position) {
        QBAttachment attachment = getQBAttach(position);
        int duration = getDurationFromAttach(attachment, position);
        setDurationVideo(duration, holder);
        String url = getVideoUrl(position);
        showVideoThumbnail(holder, url, position);
    }

    protected void setDurationVideo(int duration, QBMessageViewHolder holder) {
        ((VideoAttachHolder) holder).durationView.setText(Utils.formatTimeSecondsToMinutes(duration));
    }


    protected Uri getUriFromAttach(QBAttachment attachment) {
        return Utils.getUriFromAttachPublicUrl(attachment);
    }

    protected int getDurationFromAttach(QBAttachment attachment, int position) {
        return attachment.getDuration();
    }

    protected void setDurationAudio(int duration, QBMessageViewHolder holder) {
        ((AudioAttachHolder) holder).durationView.setText(Utils.formatTimeSecondsToMinutes(duration));
    }

    private void showAudioView(QBPlaybackControlView playerView, Uri uri, int position) {
        initPlayerView(playerView, uri, position);
        if (isCurrentViewActive(position)) {
            Log.d(TAG, "showAudioView isCurrentViewActive");
            playerView.restoreState(getMediaManagerInstance().getExoPlayer());
        }
    }

    private void initPlayerView(QBPlaybackControlView playerView, Uri uri, int position) {
        playerView.releaseView();
        setViewPosition(playerView, position);
        playerView.initView(getAudioControllerInstance(), uri);
    }

    private boolean isCurrentViewActive(int position) {
        return activePlayerViewPosition == position;
    }

    private void setPlayerViewActivePosition(int activeViewPosition) {
        this.activePlayerViewPosition = activeViewPosition;
    }

    private void setViewPosition(QBPlaybackControlView view, int position) {
        if (playerViewHashMap == null) {
            playerViewHashMap = new WeakHashMap<>();
        }
        playerViewHashMap.put(view, position);
    }

    private int getPlayerViewPosition(QBPlaybackControlView view) {
        Integer position = playerViewHashMap.get(view);
        return position == null ? activePlayerViewPosition : position;
    }

    private void showVideoThumbnail(final QBMessageViewHolder holder, String url, int position) {

        int preferredImageWidth = (int) context.getResources().getDimension(R.dimen.attach_image_width_preview);
        int preferredImageHeight = (int) context.getResources().getDimension(R.dimen.attach_image_height_preview);

        VideoThumbnail model = new VideoThumbnail(url);
        Glide.with(context)
                .load(model)
                .listener(this.<VideoThumbnail, GlideDrawable>getVideoThumbnailRequestListener(holder, position))
                .override(preferredImageWidth, preferredImageHeight)
                .dontTransform()
                .error(R.drawable.ic_error)
                .into(((VideoAttachHolder) holder).attachImageView);
    }

    public SingleMediaManager getMediaManagerInstance() {
        return mediaManager = mediaManager == null ? new SingleMediaManager(context) : mediaManager;
    }

    private AudioController getAudioControllerInstance() {
        return audioController = audioController == null ? new AudioController(getMediaManagerInstance(), getMediaControllerEventListenerInstance())
                : audioController;
    }

    private MediaControllerEventListener getMediaControllerEventListenerInstance() {
        return mediaControllerEventListener = mediaControllerEventListener == null ? new MediaControllerEventListener() : mediaControllerEventListener;
    }


    protected void showPhotoAttach(QBMessageViewHolder holder, int position) {
        String imageUrl = getImageUrl(position);
        showImageByURL(holder, imageUrl, position);
    }

    protected void showLocationAttach(QBMessageViewHolder holder, int position) {
        String locationUrl = getLocationUrl(position);
        showImageByURL(holder, locationUrl, position);
    }

    public String getImageUrl(int position) {
        QBAttachment attachment = getQBAttach(position);
        return QBFile.getPrivateUrlForUID(attachment.getId());
    }

    public String getVideoUrl(int position) {
        QBAttachment attachment = getQBAttach(position);
        return QBFile.getPrivateUrlForUID(attachment.getId());
    }

    public String getLocationUrl(int position) {
        QBAttachment attachment = getQBAttach(position);

        LocationUtils.BuilderParams params = LocationUtils.defaultUrlLocationParams(context);

        return LocationUtils.getRemoteUri(attachment.getData(), params);
    }

    protected QBAttachment getQBAttach(int position) {
        T chatMessage = getItem(position);
        return chatMessage.getAttachments().iterator().next();
    }

    private void showImageByURL(QBMessageViewHolder holder, String url, int position) {
        int preferredImageWidth = (int) context.getResources().getDimension(R.dimen.attach_image_width_preview);
        int preferredImageHeight = (int) context.getResources().getDimension(R.dimen.attach_image_height_preview);

        Glide.with(context)
                .load(url)
                .listener(this.<String, GlideDrawable>getRequestListener(holder, position))
                .override(preferredImageWidth, preferredImageHeight)
                .dontTransform()
                .error(R.drawable.ic_error)
                .into(((BaseImageAttachHolder) holder).attachImageView);
    }


    protected RequestListener getRequestListener(QBMessageViewHolder holder, int position) {
        return new ImageLoadListener<>((BaseImageAttachHolder) holder);
    }

    protected RequestListener getVideoThumbnailRequestListener(QBMessageViewHolder holder, int position) {
        return new VideoThumbnailLoadListener((VideoAttachHolder) holder);
    }

    /**
     * displayAvatarImage must be implemented in derived class
     */
    @Override
    public void displayAvatarImage(String url, ImageView imageView) {
        Glide.with(context)
                .load(url)
                .placeholder(R.drawable.placeholder_user)
                .dontAnimate()
                .into(imageView);
    }

    protected void setItemAttachClickListener(QBChatAttachClickListener listener, QBMessageViewHolder holder, QBAttachment qbAttachment, int position) {
        if (listener != null) {
            holder.bubbleFrame.setOnClickListener(new QBItemClickListenerFilter(listener, qbAttachment, position));
        }
    }

    protected void setItemAttachAudioClickListener(QBChatAttachClickListener listener, QBMessageViewHolder holder, QBAttachment qbAttachment, int position,
                                                   QBPlaybackControlView controlView) {
        holder.bubbleFrame.setOnClickListener(new QBItemAudioClickListener(listener, qbAttachment, position, controlView));
    }


    protected static class TextMessageHolder extends QBMessageViewHolder {
        public View linkPreviewLayout;
        public TextView messageTextView;
        public TextView timeTextMessageTextView;

        public TextMessageHolder(View itemView, @IdRes int msgId, @IdRes int timeId, @IdRes int linkPreviewLayoutId) {
            super(itemView);
            messageTextView = (TextView) itemView.findViewById(msgId);
            timeTextMessageTextView = (TextView) itemView.findViewById(timeId);
            linkPreviewLayout = itemView.findViewById(linkPreviewLayoutId);
        }
    }

    protected static class BaseAttachHolder extends QBMessageViewHolder {
        public TextView attachTextTime;
        public ImageView signAttachView;

        public BaseAttachHolder(View itemView, @IdRes int timeId, @IdRes int signViewId) {
            super(itemView);
            attachTextTime = (TextView) itemView.findViewById(timeId);
            signAttachView = (ImageView) itemView.findViewById(signViewId);
        }
    }

    protected static class BaseImageAttachHolder extends BaseAttachHolder {
        public ImageView attachImageView;
        public ProgressBar attachmentProgressBar;

        public BaseImageAttachHolder(View itemView, @IdRes int attachId, @IdRes int progressBarId, @IdRes int timeId, @IdRes int signId) {
            super(itemView, timeId, signId);
            attachImageView = (ImageView) itemView.findViewById(attachId);
            attachmentProgressBar = (ProgressBar) itemView.findViewById(progressBarId);
        }
    }

    protected static class ImageAttachHolder extends BaseImageAttachHolder {

        public ImageAttachHolder(View itemView, @IdRes int attachId, @IdRes int progressBarId, @IdRes int timeId, @IdRes int signId) {
            super(itemView, attachId, progressBarId, timeId, signId);
        }
    }

    public static class AudioAttachHolder extends BaseAttachHolder {
        public QBPlaybackControlView playerView;
        public TextView durationView;

        public AudioAttachHolder(View itemView, @IdRes int attachId, @IdRes int durationId, @IdRes int timeId, @IdRes int signId) {
            super(itemView, timeId, signId);
            playerView = (QBPlaybackControlView) itemView.findViewById(attachId);
            durationView = (TextView) itemView.findViewById(durationId);
        }
    }

    protected static class VideoAttachHolder extends BaseImageAttachHolder {
        public ImageView playIcon;
        public TextView durationView;

        public VideoAttachHolder(View itemView, @IdRes int attachId, @IdRes int progressBarId, @IdRes int durationId, @IdRes int timeId, @IdRes int signId, @IdRes int playIconId) {
            super(itemView, attachId, progressBarId, timeId, signId);
            playIcon = (ImageView) itemView.findViewById(playIconId);
            durationView = (TextView) itemView.findViewById(durationId);
        }
    }

    protected abstract static class QBMessageViewHolder extends RecyclerView.ViewHolder {
        public ImageView avatar;
        public View bubbleFrame;

        public QBMessageViewHolder(View itemView) {
            super(itemView);
            avatar = (ImageView) itemView.findViewById(R.id.msg_image_avatar);
            bubbleFrame = itemView.findViewById(R.id.msg_bubble_background);
        }
    }

    protected static class ImageLoadListener<M, P> implements RequestListener<M, P> {
        private BaseImageAttachHolder holder;

        protected ImageLoadListener(BaseImageAttachHolder holder) {
            this.holder = holder;
            holder.attachmentProgressBar.setVisibility(View.VISIBLE);
        }

        @Override
        public boolean onException(Exception e, M model, Target<P> target, boolean isFirstResource) {
            Log.e(TAG, "ImageLoadListener Exception= " + e);
            holder.attachImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
            holder.attachmentProgressBar.setVisibility(View.GONE);
            return false;
        }

        @Override
        public boolean onResourceReady(P resource, M model, Target<P> target, boolean isFromMemoryCache, boolean isFirstResource) {
            holder.attachImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
            holder.attachmentProgressBar.setVisibility(View.GONE);
            return false;
        }
    }

    protected static class VideoThumbnailLoadListener extends ImageLoadListener<VideoThumbnail, GlideDrawable> {
        private VideoAttachHolder holder;

        protected VideoThumbnailLoadListener(VideoAttachHolder holder) {
            super(holder);
            this.holder = holder;
            holder.playIcon.setVisibility(View.INVISIBLE);
        }

        @Override
        public boolean onResourceReady(GlideDrawable resource, VideoThumbnail model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
            super.onResourceReady(resource, model, target, isFromMemoryCache, isFirstResource);
            holder.playIcon.setVisibility(View.VISIBLE);
            return false;
        }
    }

    private class QBItemAudioClickListener extends QBItemClickListenerFilter {
        private QBPlaybackControlView controlView;

        QBItemAudioClickListener(QBChatAttachClickListener chatAttachClickListener, QBAttachment attachment, int position, QBPlaybackControlView controlView) {
            super(chatAttachClickListener, attachment, position);
            this.controlView = controlView;
        }

        @Override
        public void onClick(View view) {
            if (chatAttachClickListener != null) {
                super.onClick(view);
            }
            controlView.clickIconPlayPauseView();
        }
    }

    private class QBItemClickListenerFilter implements View.OnClickListener {
        protected int position;
        protected QBAttachment attachment;
        protected QBChatAttachClickListener chatAttachClickListener;

        QBItemClickListenerFilter(QBChatAttachClickListener qbChatAttachClickListener, QBAttachment attachment, int position) {
            this.position = position;
            this.attachment = attachment;
            this.chatAttachClickListener = qbChatAttachClickListener;
        }

        @Override
        public void onClick(View view) {
            chatAttachClickListener.onLinkClicked(attachment, position);
        }
    }

    private class MediaControllerEventListener implements MediaController.EventMediaController {

        @Override
        public void onPlayerInViewInit(QBPlaybackControlView view) {
            setPlayerViewActivePosition(getPlayerViewPosition(view));
        }
    }

    private class QBLinkPreviewClickListenerFilter implements View.OnClickListener, View.OnLongClickListener {
        private final QBLinkPreviewClickListener linkPreviewClickListener;
        private final QBLinkPreview linkPreview;
        private final int position;
        private final String link;

        public QBLinkPreviewClickListenerFilter(QBLinkPreviewClickListener linkPreviewClickListener, String link, QBLinkPreview linkPreview, int position) {
            this.linkPreviewClickListener = linkPreviewClickListener;
            this.link = link;
            this.linkPreview = linkPreview;
            this.position = position;
        }

        @Override
        public void onClick(View view) {
            if (linkPreviewClickListener != null) {
                linkPreviewClickListener.onLinkPreviewClicked(link, linkPreview, position);
            }

            if (!overrideOnLinkPreviewClick) {
                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link));
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                context.startActivity(intent);
            }
        }

        @Override
        public boolean onLongClick(View v) {
            if (linkPreviewClickListener != null) {
                linkPreviewClickListener.onLinkPreviewLongClicked(link, linkPreview, position);
            }

            return true;
        }
    }

    private class LoadLinkPreviewHandler implements QBEntityCallback<QBLinkPreview> {
        private TextMessageHolder holder;
        private int position;
        private String firstLink;
        private List<String> urlsList;
        private final String viewIdentifier;

        public LoadLinkPreviewHandler(TextMessageHolder holder, List<String> urlsList, int position, String viewIdentifier) {
            this.holder = holder;
            this.urlsList = urlsList;
            this.position = position;
            this.firstLink = LinkUtils.getLinkWithProtocol(urlsList.get(0));
            this.viewIdentifier = viewIdentifier;
        }

        @Override
        public void onSuccess(QBLinkPreview linkPreview, Bundle bundle) {
            if (linkPreview != null
                    && holder.linkPreviewLayout.getTag() != null
                    && holder.linkPreviewLayout.getTag().equals(viewIdentifier)) {
                holder.linkPreviewLayout.setOnClickListener(new QBLinkPreviewClickListenerFilter(linkPreviewClickListener, firstLink, linkPreview, position));
                holder.linkPreviewLayout.setOnLongClickListener(new QBLinkPreviewClickListenerFilter(linkPreviewClickListener, firstLink, linkPreview, position));

                fillLinkPreviewLayout(holder.linkPreviewLayout, linkPreview, firstLink);

                AnimationsUtils.showView(holder.linkPreviewLayout, SHOW_VIEW_ANIMATION_DURATION);
            }
        }

        @Override
        public void onError(QBResponseException e) {
            //ignore error
        }
    }
} public interface QBLinkPreviewClickListener {

    void onLinkPreviewClicked(String link, QBLinkPreview linkPreview, int position);

    void onLinkPreviewLongClicked(String link, QBLinkPreview linkPreview, int position);
} public class QBLinkPreview implements Serializable{

    @SerializedName("ogTitle")
    private String title;

    @SerializedName("ogDescription")
    private String description;

    @SerializedName("ogImage")
    private QBLinkPreviewImage image;

    @SerializedName("ogLocale")
    private String locale;

    @SerializedName("ogSiteName")
    private String siteName;

    @SerializedName("ogUrl")
    private String url;

    @SerializedName("ogType")
    private String type;     //possible "site", "website"

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public QBLinkPreviewImage getImage() {
        return image;
    }

    public void setImage(QBLinkPreviewImage image) {
        this.image = image;
    }

    public String getLocale() {
        return locale;
    }

    public void setLocale(String locale) {
        this.locale = locale;
    }

    public String getSiteName() {
        return siteName;
    }

    public void setSiteName(String siteName) {
        this.siteName = siteName;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder(QBLinkPreview.class.getSimpleName());
        stringBuilder.append("{").append("title").append("=").append(getTitle()).
                append(", description").append("=").append(getDescription()).
                append(", image").append("=").append(getImage()).
                append(", locale").append("=").append(getLocale()).
                append(", siteName").append("=").append(getSiteName()).
                append(", url").append("=").append(getUrl()).
                append(", type").append("=").append(getType()).
                append("}");
        return stringBuilder.toString();
    }
} public interface QBChatAttachClickListener {

    void onLinkClicked(QBAttachment attachment, int positionInAdapter);
} public interface QBChatMessageLinkClickListener {

    /**
     * This method will be invoked when user press and hold
     * finger on the {@link TextView}
     *
     * @param linkText          Text which contains link on which user presses.
     * @param linkType          Type of the link can be one of {@link QBMessageTextClickMovement.QBLinkType} enumeration
     * @param positionInAdapter Index of item with this TextView in message adapter
     */
    void onLinkClicked(final String linkText, final QBMessageTextClickMovement.QBLinkType linkType, int positionInAdapter);

    /**
     * @param text              Whole text of {@link TextView}
     * @param positionInAdapter Index of item with this TextView in message adapter
     */
    void onLongClick(final String text, int positionInAdapter);
} public interface QBMediaPlayerListener {

    void onStart(Uri uri);

    void onResume(Uri uri);

    void onPause(Uri uri);

    void onStop(Uri uri);

    void onPlayerError(ExoPlaybackException error);
} public class SingleMediaManager implements MediaManager, Player.EventListener {
    private static String TAG = SingleMediaManager.class.getSimpleName();

    private QBPlaybackControlView playerView;
    private SimpleExoPlayer exoPlayer;
    private Context context;

    private Uri uri;
    private boolean shouldAutoPlay;
    private int resumeWindow;
    private long resumePosition;
    private final CopyOnWriteArraySet<QBMediaPlayerListener> listeners;

    public SingleMediaManager(Context context) {
        listeners = new CopyOnWriteArraySet<>();
        this.context = context;
    }

    public void addListener(QBMediaPlayerListener listener) {
        listeners.add(listener);
    }

    public void removeListener(QBMediaPlayerListener listener) {
        listeners.remove(listener);
    }

    public SimpleExoPlayer getExoPlayer() {
        return exoPlayer;
    }

    @Override
    public void playMedia(QBPlaybackControlView playerView, Uri uri) {
        if (isPlayerViewCurrent(playerView)) {
            if (isPlaying()) {
                Log.v(TAG, "playMedia: already playing");
                return;
            } else {
                Log.v(TAG, "playMedia: continue playing");
                exoPlayer.setPlayWhenReady(true);

                notifyListenersOnResume();
                return;
            }
        }
        this.uri = uri;
        updatePlayerView(playerView);
        stopResetCurrentPlayer();
        initPlayer();
        initViewPlayback(playerView);
        startPlayback();

        notifyListenersOnStart();
    }

    @Override
    public void pauseMedia() {
        if (exoPlayer != null) {
            exoPlayer.setPlayWhenReady(false);
            notifyListenersOnPause();
        }
    }

    public void suspendPlay() {
        resetMediaPlayer();
        notifyListenersOnPause();
    }

    public void resumePlay() {
        restoreMediaPlayer();
        notifyListenersOnResume();
    }


    @Override
    public void stopAnyPlayback() {
        stopResetCurrentPlayer();
    }

    @Override
    public void resetMediaPlayer() {
        Log.v(TAG, "resetMediaPlayer: should clear clearPlayerInstance");
        releasePlayer();
    }

    private boolean isPlayerViewCurrent(QBPlaybackControlView playerView) {
        return exoPlayer != null && this.playerView != null && this.playerView == playerView && playerView.isCurrentViewPlaying();
    }

    public boolean isMediaPlayerReady() {
        return uri != null;
    }

    public boolean isPlaying() {
        if (exoPlayer != null) {
            Log.d(TAG, "isPlaying playbackState= " + exoPlayer.getPlaybackState());
            return exoPlayer.getPlayWhenReady();
        }
        return false;
    }

    private void updatePlayerView(QBPlaybackControlView playerView) {
        if (this.playerView != null && this.playerView != playerView) {
            this.playerView.setDurationViewOnTop();
        }
    }

    private void stopResetCurrentPlayer() {
        Log.v(TAG, "stopResetCurrentPlayer");
        if (playerView != null) {
            releasePlayer();
            playerView.disposeViewPlayer();
        }
    }

    private void releasePlayer() {
        if (exoPlayer != null) {
            shouldAutoPlay = exoPlayer.getPlayWhenReady();
            updateResumePosition();
            exoPlayer.release();
            removeEventListener();
            exoPlayer = null;
        }
    }

    private void restoreMediaPlayer() {
        initPlayer();
        if (isPlayerViewCurrent(playerView)) {
            playerView.setPlayer(exoPlayer);
        }
        MediaSource mediaSource = SimpleExoPlayerInitializer.buildMediaSource(uri, context);
        boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
        if (haveResumePosition) {
            exoPlayer.seekTo(resumeWindow, resumePosition);
        }
        exoPlayer.prepare(mediaSource, !haveResumePosition, false);
        exoPlayer.setPlayWhenReady(shouldAutoPlay);
    }

    private void updateResumePosition() {
        resumeWindow = exoPlayer.getCurrentWindowIndex();
        resumePosition = exoPlayer.isCurrentWindowSeekable() ? Math.max(0, exoPlayer.getCurrentPosition())
                : C.TIME_UNSET;
    }

    private void initViewPlayback(QBPlaybackControlView playerView) {
        Log.v(TAG, "initViewPlayback");
        this.playerView = playerView;
        playerView.setPlayer(exoPlayer);
    }

    private void startPlayback() {
        exoPlayer.prepare(SimpleExoPlayerInitializer.buildMediaSource(uri, context));
        exoPlayer.setPlayWhenReady(true);
    }

    private void initPlayer() {
        exoPlayer = SimpleExoPlayerInitializer.initializeExoPlayer(context);
        setEventListener();
    }

    private void setEventListener() {
        exoPlayer.addListener(this);
    }

    private void removeEventListener() {
        exoPlayer.removeListener(this);
    }

    public void onStartPosition() {
        if(exoPlayer != null) {
            exoPlayer.seekTo(0);
            pauseMedia();
        }
    }

    private void notifyListenersOnStart() {
        for (QBMediaPlayerListener listener : listeners) {
            listener.onStart(uri);
        }
    }

    private void notifyListenersOnResume() {
        for (QBMediaPlayerListener listener : listeners) {
            listener.onResume(uri);
        }
    }

    private void notifyListenersOnPause() {
        for (QBMediaPlayerListener listener : listeners) {
            listener.onPause(uri);
        }
    }

    private void notifyListenersOnStop() {
        for (QBMediaPlayerListener listener : listeners) {
            listener.onStop(uri);
        }
    }

    private void notifyListenersOnPlayerError(ExoPlaybackException error) {
        for (QBMediaPlayerListener listener : listeners) {
            listener.onPlayerError(error);
        }
    }

    @Override
    public void onTimelineChanged(Timeline timeline, Object manifest) {

    }

    @Override
    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {

    }

    @Override
    public void onLoadingChanged(boolean isLoading) {

    }

    @Override
    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
        parsePlayerEvent(playbackState);
    }

    @Override
    public void onRepeatModeChanged(int repeatMode) {

    }

    @Override
    public void onPlayerError(ExoPlaybackException error) {
        notifyListenersOnPlayerError(error);
    }

    @Override
    public void onPositionDiscontinuity() {

    }

    @Override
    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {

    }

    private void parsePlayerEvent(int playbackState) {
        switch (playbackState) {
            case Player.STATE_BUFFERING:
                break;
            case Player.STATE_ENDED:
                notifyListenersOnStop();
                break;
            case Player.STATE_IDLE:
                break;
            case Player.STATE_READY:
                break;
            default:
                break;
        }
    }
} public class AudioRecorder extends QBMediaRecorder<AudioRecorder> {
    private static final String TAG = AudioRecorder.class.getSimpleName();

    private QBMediaRecordListener recordListener;
    private MediaRecorder recorder = null;
    private RecordState state = RecordState.RECORD_STATE_UNKNOWN;
    private ConfigurationBuilder configurationBuilder;

    private AudioRecorder() {
    }

    public static ConfigurationBuilder newBuilder() {
        return new AudioRecorder().new ConfigurationBuilder();
    }

    @Override
    public void startRecord() {
        setState(RecordState.RECORD_STATE_BEGIN);
        initMediaRecorder();
        prepareStartMediaRecorder();
    }

    @Override
    public void cancelRecord() {
        stopAndReleaseMediaRecorder();
        setState(RecordState.RECORD_STATE_UNKNOWN);
        if (recordListener != null) {
            recordListener.onMediaRecordClosed();
        }
    }

    @Override
    public void stopRecord() {
        stopAndReleaseMediaRecorder();
        sendResult();
    }

    private void initMediaRecorder() {
        recorder = new MediaRecorder();
        recorder.setAudioSource(configurationBuilder.audioSource);
        recorder.setOutputFormat(configurationBuilder.outputFormat);
        recorder.setOutputFile(configurationBuilder.filePath);
        recorder.setAudioChannels(configurationBuilder.channels);
        recorder.setAudioEncoder(configurationBuilder.audioEncoder);
        recorder.setAudioEncodingBitRate(configurationBuilder.bitRate);
        recorder.setAudioSamplingRate(configurationBuilder.samplingRate);
        recorder.setMaxDuration(configurationBuilder.duration);
        recorder.setOnInfoListener(new OnInfoListenerImpl());
        recorder.setOnErrorListener(new OnErrorListenerImpl());
    }

    private void prepareStartMediaRecorder() {
        try {
            recorder.prepare();
            recorder.start();
        } catch (IOException e) {
            notifyListenerError(new MediaRecorderException(e.getMessage()));
        }
    }

    private void notifyListenerError(MediaRecorderException e) {
        setState(RecordState.RECORD_STATE_ERROR);
        if (recordListener != null) {
            recordListener.onMediaRecordError(e);
        }
    }

    private void notifyListenerSuccess() {
        setState(RecordState.RECORD_STATE_COMPLETED);
        if (recordListener != null) {
            recordListener.onMediaRecorded(getFile());
        }
    }

    private File getFile() {
        return new File(configurationBuilder.filePath);
    }

    // From MediaRecorder: RuntimeException is intentionally thrown to the application,
    // if no valid audio/video data has been received when stop() is called.
    // This happens if stop() is called immediately after start()
    private void stopAndReleaseMediaRecorder() {
        if (recorder != null) {
            if (state.ordinal() >= RecordState.RECORD_STATE_BEGIN.ordinal()) {
                try {
                    recorder.stop();
                } catch (RuntimeException stopException) {
                    notifyListenerError(new MediaRecorderException(stopException.getMessage()));
                }
            }
            releaseMediaRecorder();
        }
    }

    public void releaseMediaRecorder() {
        if (recorder != null) {
            recorder.release();
            recorder = null;
            Log.d(TAG, "mediaRecorder released");
        }
    }

    public void setMediaRecordListener(QBMediaRecordListener recordListener) {
        this.recordListener = recordListener;
    }

    public void removeMediaRecordListener() {
        this.recordListener = null;
    }

    public boolean isRecording() {
        return state.equals(RecordState.RECORD_STATE_BEGIN);
    }

    private void sendResult() {
        if (state.ordinal() > RecordState.RECORD_STATE_UNKNOWN.ordinal() && state.ordinal() < RecordState.RECORD_STATE_COMPLETED.ordinal()) {
            notifyListenerSuccess();
        }
    }

    private class OnErrorListenerImpl implements MediaRecorder.OnErrorListener {

        @Override
        public void onError(MediaRecorder mediaRecorder, int what, int extra) {
            String error = Utils.parseCode(what);
            notifyListenerError(new MediaRecorderException(error));
        }
    }

    private class OnInfoListenerImpl implements MediaRecorder.OnInfoListener {

        @Override
        public void onInfo(MediaRecorder mediaRecorder, int what, int extra) {
            String event = Utils.parseCode(what);
            Log.d(TAG, "onInfo event= " + event + ", extra= " + extra);
            if (!canProceed(what)) {
                stopAndReleaseMediaRecorder();
                sendResult();
            }
        }

        private boolean canProceed(int what) {
            return what == Utils.MEDIA_RECORDING_IS_IN_PROGRESS;
        }
    }

    private void setState(RecordState state) {
        this.state = state;
    }

    private enum RecordState {
        RECORD_STATE_UNKNOWN,
        RECORD_STATE_BEGIN,
        RECORD_STATE_COMPLETED,
        RECORD_STATE_ERROR;
    }

    public class ConfigurationBuilder {
        public static final int CHANNEL_MONO = 1;
        public static final int CHANNEL_STEREO = 2;

        private static final String DEFAULT_FILE_NAME = "recorded_audio_chat.mp3";

        private int channels = CHANNEL_STEREO;
        private String filePath;

        private int audioSource = MediaRecorder.AudioSource.MIC;
        private int outputFormat = MediaRecorder.OutputFormat.MPEG_4;
        private int audioEncoder = MediaRecorder.AudioEncoder.AAC;
        private int bitRate = 96000;
        private int samplingRate = 44100;
        private int duration = (int) TimeUnit.SECONDS.toMillis(30);

        private ConfigurationBuilder() {
        }

        public ConfigurationBuilder useInBuildFilePathGenerator(Context context) {
            filePath = Utils.getAudioPathPrivate(context, DEFAULT_FILE_NAME);
            return this;
        }

        public ConfigurationBuilder setFilePath(String filePath) {
            this.filePath = filePath;
            return this;
        }

        public ConfigurationBuilder setAudioSource(int source) {
            this.audioSource = source;
            return this;
        }

        public ConfigurationBuilder setOutputFormat(int outputFormat) {
            this.outputFormat = outputFormat;
            return this;
        }

        public ConfigurationBuilder setAudioEncoder(int audioEncoder) {
            this.audioEncoder = audioEncoder;
            return this;
        }

        public ConfigurationBuilder setAudioEncodingBitRate(int bitRate) {
            this.bitRate = bitRate;
            return this;
        }

        public ConfigurationBuilder setDuration(int durationSeconds) {
            this.duration = (int) TimeUnit.SECONDS.toMillis(durationSeconds);
            return this;
        }

        public ConfigurationBuilder setAudioChannels(int channels) {
            this.channels = channels;
            return this;
        }

        public ConfigurationBuilder setAudioSamplingRate(int samplingRate) {
            this.samplingRate = samplingRate;
            return this;
        }

        public AudioRecorder build() {
            AudioRecorder.this.configurationBuilder = this;
            return AudioRecorder.this;
        }

    }
} public class MediaRecorderException extends Exception {

    public MediaRecorderException(String s) {
        super(s);
    }
} public interface QBMediaRecordListener {

    void onMediaRecorded(File file);

    void onMediaRecordError(MediaRecorderException e);

    void onMediaRecordClosed();
} public class QBRecordAudioButton extends ImageButton {

    private RecordTouchEventListener recordTouchEventListener;
    private final RecordTouchListener recordTouchListener;
    private final RecordLongClickListener recordLongClickListener;

    private final float halfWidthScreen;
    private boolean isSpeakButtonLongPressed;


    public interface RecordTouchEventListener {
        void onStartClick(View view);

        void onCancelClick(View view);

        void onStopClick(View view);
    }

    public QBRecordAudioButton(Context context) {
        this(context, null);
    }

    public QBRecordAudioButton(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public QBRecordAudioButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        halfWidthScreen = getMiddleWidthScreen();
        recordTouchListener = new RecordTouchListener();
        recordLongClickListener = new RecordLongClickListener();
    }

    public void setRecordTouchListener(final RecordTouchEventListener recordTouchEventListener) {
        this.recordTouchEventListener = recordTouchEventListener;
        initListeners();
    }

    private void initListeners() {
        setOnLongClickListener();
        setOnTouchListener();
    }

    private void setOnTouchListener() {
        this.setOnTouchListener(recordTouchListener);
    }

    private void setOnLongClickListener() {
        this.setOnLongClickListener(recordLongClickListener);
    }

    private boolean canCancel(float x, float halfWidthScreen) {
        return x < halfWidthScreen;
    }

    private float getMiddleWidthScreen() {
        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
        return displayMetrics.widthPixels / 2;
    }

    private final class RecordTouchListener implements View.OnTouchListener {

        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            view.onTouchEvent(motionEvent);

            float xRaw = motionEvent.getRawX();

            switch (motionEvent.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    if (canCancel(xRaw, halfWidthScreen) && isSpeakButtonLongPressed) {
                        recordTouchEventListener.onCancelClick(view);
                        isSpeakButtonLongPressed = false;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    if (isSpeakButtonLongPressed) {
                        recordTouchEventListener.onStopClick(view);
                        isSpeakButtonLongPressed = false;
                    }
                    break;
            }
            return true;
        }
    }

    private final class RecordLongClickListener implements View.OnLongClickListener {

        @Override
        public boolean onLongClick(View v) {
            isSpeakButtonLongPressed = true;
            recordTouchEventListener.onStartClick(v);

            return false;
        }
    }
} public class VideoPlayerActivity extends Activity {
    private static final String EXTRA_VIDEO_URI = "video_uri";
    private SimpleExoPlayerView simpleExoPlayerView;
    private TextureView textureView;
    private SimpleExoPlayer player;

    private Uri videoUri;
    private boolean shouldAutoPlay;
    private int resumeWindow;
    private long resumePosition;

    public static void start(Context context, Uri videoUri) {
        Intent intent = new Intent(context, VideoPlayerActivity.class);
        intent.putExtra(EXTRA_VIDEO_URI, videoUri);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.widget_player_activity);
        setScreenOrientationPortrait();
        initViews();
        initFields();
    }

    @Override
    public void onStart() {
        super.onStart();
        if (Util.SDK_INT > 23) {
            initializePlayer();
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        if ((Util.SDK_INT <= 23 || player == null)) {
            initializePlayer();
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        if (Util.SDK_INT <= 23) {
            releasePlayer();
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        if (Util.SDK_INT > 23) {
            releasePlayer();
        }
    }

    private void setScreenOrientationPortrait() {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    }

    private void initViews() {
        simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view);
        textureView = (TextureView) simpleExoPlayerView.getVideoSurfaceView();
    }

    private void initFields() {
        videoUri = getIntent().getParcelableExtra(EXTRA_VIDEO_URI);
        shouldAutoPlay = true;
    }

    private void initializePlayer() {
        player = SimpleExoPlayerInitializer.initializeExoPlayer(this);
        player.addListener(new PlayerStateListener());
        player.setVideoDebugListener(new VideoListener());
        simpleExoPlayerView.setPlayer(player);
        player.setPlayWhenReady(shouldAutoPlay);
        boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
        if (haveResumePosition) {
            player.seekTo(resumeWindow, resumePosition);
        }
        player.prepare(SimpleExoPlayerInitializer.buildMediaSource(videoUri, this));
    }

    private void releasePlayer() {
        if (player != null) {
            shouldAutoPlay = player.getPlayWhenReady();
            updateResumePosition();
            player.release();
            player = null;
        }
    }

    private void updateResumePosition() {
        resumeWindow = player.getCurrentWindowIndex();
        resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition())
                : C.TIME_UNSET;
    }

    private void setPlayerToStartPosition() {
        player.seekTo(0);
        player.setPlayWhenReady(false);
    }

    private class PlayerStateListener extends ExoPlayerEventListenerImpl {

        @Override
        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
            if (playbackState == Player.STATE_ENDED) {
                setPlayerToStartPosition();
            }
        }
    }

    private class VideoListener extends VideoRendererEventListenerImpl {
        private int rotationDegree;

        @Override
        public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
            rotationDegree = unappliedRotationDegrees;
            updateSurfaceViewIfNeed();
        }

        private void updateSurfaceViewIfNeed() {
            if (needUpdateSurfaceView()) {
                adjustSurfaceView();
                setCorrectRotation();
            }
        }

        private void adjustSurfaceView() {
            simpleExoPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL);
        }

        private void setCorrectRotation() {
            int viewWidth = textureView.getWidth();
            int viewHeight = textureView.getHeight();

            Matrix matrix = new Matrix();
            matrix.reset();
            int px = viewWidth / 2;
            int py = viewHeight / 2;
            matrix.postRotate(rotationDegree, px, py);

            float ratio = (float) viewHeight / viewWidth;
            matrix.postScale(1 / ratio, ratio, px, py);

            textureView.setTransform(matrix);
        }

        private boolean needUpdateSurfaceView() {
            return rotationDegree == 90 || rotationDegree == 270;
        }
    }
} public class QBMessageTextClickMovement extends ArrowKeyMovementMethod {

    private final GestureDetector gestureDetector;
    private final boolean overrideOnLinkClick;
    private TextView textView;
    private Spannable buffer;
    private int positionInAdapter = -1;


    public QBMessageTextClickMovement(final QBChatMessageLinkClickListener listener, boolean overrideOnClick, final Context context) {
        this.overrideOnLinkClick = overrideOnClick;
        this.gestureDetector = new GestureDetector(context, new SimpleOnGestureListener(listener));
    }

    public void setPositionInAdapter(int positionInAdapter) {
        this.positionInAdapter = positionInAdapter;
    }

    private int getPositionInAdapter() {
        return positionInAdapter;
    }

    @Override
    public boolean onTouchEvent(final TextView widget, final Spannable buffer, final MotionEvent event) {
        this.textView = widget;
        this.buffer = buffer;
        gestureDetector.onTouchEvent(event);

        //Return super method for delegate logic sending intent from Linkify
        //or return 'false' for yourself managing logic by link clicked
        if (overrideOnLinkClick) {
            return false;
        } else {
            return super.onTouchEvent(widget, buffer, event);
        }
    }

    /**
     * Detects various gestures and events.
     * Notify users when a particular motion event has occurred.
     */
    class SimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener {

        private final QBChatMessageLinkClickListener linkClickListener;

        public SimpleOnGestureListener(QBChatMessageLinkClickListener linkClickListener) {
            this.linkClickListener = linkClickListener;
        }

        @Override
        public boolean onDown(MotionEvent event) {
            // Notified when a tap occurs.
            return false;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            // Notified when a long press occurs.
            final String text = buffer.toString();

            if (linkClickListener != null) {
                Lo.g("Long Click Occurs on TextView with ID: " + textView.getId() +
                        " Text: " + text);

                linkClickListener.onLongClick(text, getPositionInAdapter());
            }
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent event) {
            // Notified when tap occurs.
            final String linkText = getLinkText(textView, buffer, event);

            QBLinkType linkType = QBLinkType.NONE;

            if (Patterns.PHONE.matcher(linkText).matches()) {
                linkType = QBLinkType.PHONE;
            } else if (Patterns.WEB_URL.matcher(linkText).matches()) {
                linkType = QBLinkType.WEB_URL;
            } else if (Patterns.EMAIL_ADDRESS.matcher(linkText).matches()) {
                linkType = QBLinkType.EMAIL_ADDRESS;
            }

            if (linkClickListener != null) {
                Lo.g("Tap Occurs on TextView with ID: " + textView.getId() +
                        " Link Text: " + linkText +
                        " Link Type: " + linkType +
                        " Position: " + getPositionInAdapter());

                linkClickListener.onLinkClicked(linkText, linkType, getPositionInAdapter());
            }

            return false;
        }

        private String getLinkText(final TextView widget, final Spannable buffer, final MotionEvent event) {

            int x = (int) event.getX();
            int y = (int) event.getY();

            x -= widget.getTotalPaddingLeft();
            y -= widget.getTotalPaddingTop();

            x += widget.getScrollX();
            y += widget.getScrollY();

            Layout layout = widget.getLayout();
            int line = layout.getLineForVertical(y);
            int off = layout.getOffsetForHorizontal(line, x);

            ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);

            if (link.length != 0) {
                return buffer.subSequence(buffer.getSpanStart(link[0]),
                        buffer.getSpanEnd(link[0])).toString();
            }

            return buffer.toString();
        }
    }

    public enum QBLinkType {

        /**
         * Indicates that phone link was clicked
         */
        PHONE,

        /**
         * Identifies that URL was clicked
         */
        WEB_URL,

        /**
         * Identifies that Email Address was clicked
         */
        EMAIL_ADDRESS,

        /**
         * Indicates that none of above mentioned were clicked
         */
        NONE
    }
}
import android.Manifest;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.os.Vibrator;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Chronometer;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.quickblox.chat.model.QBAttachment;
import com.quickblox.chat.model.QBChatDialog;
import com.quickblox.chat.model.QBChatMessage;
import com.quickblox.content.model.QBFile;
import com.quickblox.core.QBEntityCallback;
import com.quickblox.core.exception.QBResponseException;
import com.quickblox.sample.chatadapter.R;
import com.quickblox.sample.chatadapter.ui.adapter.CustomMessageAdapter;
import com.quickblox.sample.chatadapter.utils.ChatHelper;
import com.quickblox.ui.kit.chatmessage.adapter.QBMessagesAdapter;
import com.quickblox.ui.kit.chatmessage.adapter.listeners.QBLinkPreviewClickListener;
import com.quickblox.ui.kit.chatmessage.adapter.models.QBLinkPreview;
import com.quickblox.ui.kit.chatmessage.adapter.listeners.QBChatAttachClickListener;
import com.quickblox.ui.kit.chatmessage.adapter.listeners.QBChatMessageLinkClickListener;
import com.quickblox.ui.kit.chatmessage.adapter.listeners.QBMediaPlayerListener;
import com.quickblox.ui.kit.chatmessage.adapter.media.SingleMediaManager;
import com.quickblox.ui.kit.chatmessage.adapter.media.recorder.AudioRecorder;
import com.quickblox.ui.kit.chatmessage.adapter.media.recorder.exceptions.MediaRecorderException;
import com.quickblox.ui.kit.chatmessage.adapter.media.recorder.listeners.QBMediaRecordListener;
import com.quickblox.ui.kit.chatmessage.adapter.media.recorder.view.QBRecordAudioButton;
import com.quickblox.ui.kit.chatmessage.adapter.media.video.ui.VideoPlayerActivity;
import com.quickblox.ui.kit.chatmessage.adapter.utils.QBMessageTextClickMovement;
import com.quickblox.users.model.QBUser;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import static android.widget.LinearLayout.VERTICAL;
package com.quickblox.sample.chatadapter.ui.activity;






public class ChatActivity extends AppCompatActivity {
    private static final String TAG = ChatActivity.class.getSimpleName();
    private static final String EXTRA_QB_USERS = "qb_user_list";
    private static final String DIALOG_ID = "57b701e8a0eb472505000039";

    private static final int REQUEST_RECORD_AUDIO_WRITE_EXTERNAL_STORAGE_PERMISSIONS = 200;

    private int skipPagination = 0;
    private QBChatDialog chatDialog;
    private RecyclerView messagesListView;
    private ProgressBar progressBar;
    private QBMessagesAdapter chatAdapter;
    private QBRecordAudioButton recordButton;
    private TextView audioRecordTextView;
    private SingleMediaManager mediaManager;