org.kontalk.ui.AbstractComposeFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.kontalk.ui.AbstractComposeFragment.java

Source

/*
 * Kontalk Android client
 * Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
    
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
    
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
    
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.kontalk.ui;

import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.akalipetis.fragment.ActionModeListFragment;
import com.akalipetis.fragment.MultiChoiceModeListener;
import com.nispok.snackbar.Snackbar;
import com.nispok.snackbar.SnackbarManager;
import com.nispok.snackbar.enums.SnackbarType;
import com.nispok.snackbar.listeners.ActionClickListener;

import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smackx.chatstates.ChatState;
import org.jxmpp.util.XmppStringUtils;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.AsyncQueryHandler;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.MergeCursor;
import android.database.sqlite.SQLiteDiskIOException;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.ContactsContract.Contacts;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.view.ActionMode;
import android.text.ClipboardManager;
import android.text.TextUtils;
import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import org.kontalk.Kontalk;
import org.kontalk.Log;
import org.kontalk.R;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.crypto.Coder;
import org.kontalk.data.Contact;
import org.kontalk.data.Conversation;
import org.kontalk.message.AttachmentComponent;
import org.kontalk.message.AudioComponent;
import org.kontalk.message.CompositeMessage;
import org.kontalk.message.GroupCommandComponent;
import org.kontalk.message.ImageComponent;
import org.kontalk.message.MessageComponent;
import org.kontalk.message.TextComponent;
import org.kontalk.message.VCardComponent;
import org.kontalk.provider.MessagesProviderUtils;
import org.kontalk.provider.MyMessages.Messages;
import org.kontalk.provider.MyMessages.Threads;
import org.kontalk.provider.MyMessages.Threads.Conversations;
import org.kontalk.reporting.ReportingManager;
import org.kontalk.service.DownloadService;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.ui.adapter.MessageListAdapter;
import org.kontalk.ui.view.AttachmentRevealFrameLayout;
import org.kontalk.ui.view.AudioContentView;
import org.kontalk.ui.view.AudioContentViewControl;
import org.kontalk.ui.view.AudioPlayerControl;
import org.kontalk.ui.view.ComposerBar;
import org.kontalk.ui.view.ComposerListener;
import org.kontalk.ui.view.MessageListItem;
import org.kontalk.util.MediaStorage;
import org.kontalk.util.MessageUtils;
import org.kontalk.util.Preferences;
import org.kontalk.util.SystemUtils;

import static android.content.res.Configuration.KEYBOARDHIDDEN_NO;

/**
 * Abstract message composing fragment.
 * @author Daniele Ricci
 * @author Andrea Cappelli
 */
public abstract class AbstractComposeFragment extends ActionModeListFragment
        implements ComposerListener, View.OnLongClickListener,
        // TODO these two interfaces should be handled by an inner class
        AudioDialog.AudioDialogListener, AudioPlayerControl, MultiChoiceModeListener {
    static final String TAG = ComposeMessage.TAG;

    private static final int MESSAGE_LIST_QUERY_TOKEN = 8720;
    private static final int CONVERSATION_QUERY_TOKEN = 8721;
    private static final int MESSAGE_PAGE_QUERY_TOKEN = 8723;

    /** How many messages to load per page. */
    private static final int MESSAGE_PAGE_SIZE = 1000;

    private static final int SELECT_ATTACHMENT_OPENABLE = 1;
    private static final int SELECT_ATTACHMENT_CONTACT = 2;
    private static final int SELECT_ATTACHMENT_PHOTO = 3;
    private static final int REQUEST_INVITE_USERS = 4;

    // use this as base for request codes for child classes
    protected static final int REQUEST_FIRST_CHILD = 100;

    protected enum WarningType {
        SUCCESS(0), // not implemented
        INFO(1), // not implemented
        WARNING(2), FATAL(3);

        private final int value;

        WarningType(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }
    }

    /* Attachment chooser stuff. */
    private AttachmentRevealFrameLayout mAttachmentContainer;

    protected ComposerBar mComposer;

    MessageListQueryHandler mQueryHandler;
    MessageListAdapter mListAdapter;
    /** Header view for the list view: "previous messages" button. */
    private View mHeaderView;
    private View mNextPageButton;
    private TextView mStatusText;
    private MenuItem mDeleteThreadMenu;
    private MenuItem mToggleEncryptionMenu;

    /** The thread id. */
    long threadId = -1;
    protected Conversation mConversation;
    protected String mUserName;

    /** Available resources. */
    protected Set<String> mAvailableResources = new HashSet<>();

    /** Media player stuff. */
    private int mMediaPlayerStatus = AudioContentView.STATUS_IDLE;
    private Handler mHandler;
    private Runnable mMediaPlayerUpdater;
    private AudioContentViewControl mAudioControl;
    private AudioFragment mAudioFragment;

    /** Audio recording dialog. */
    private AudioDialog mAudioDialog;

    private PeerObserver mPeerObserver;
    private File mCurrentPhoto;

    protected LocalBroadcastManager mLocalBroadcastManager;
    private BroadcastReceiver mPresenceReceiver;

    private boolean mOfflineModeWarned;
    protected CharSequence mCurrentStatus;

    private int mCheckedItemCount;

    /** Returns a new fragment instance from a picked contact. */
    public static AbstractComposeFragment fromUserId(Context context, String userId, boolean creatingGroup) {
        AbstractComposeFragment f = new ComposeMessageFragment();
        Conversation conv = Conversation.loadFromUserId(context, userId);
        // not found - create new
        if (conv == null) {
            Bundle args = new Bundle();
            args.putString("action", ComposeMessage.ACTION_VIEW_USERID);
            args.putParcelable("data", Threads.getUri(userId));
            // non existing group threads can't exist, so no reason to use creatingGroup
            f.setArguments(args);
            return f;
        }

        return fromConversation(context, conv, creatingGroup);
    }

    /** Returns a new fragment instance from a {@link Conversation} instance. */
    public static AbstractComposeFragment fromConversation(Context context, Conversation conv,
            boolean creatingGroup) {
        return fromConversation(context, conv.getThreadId(), conv.isGroupChat(), creatingGroup);
    }

    /** Returns a new fragment instance from a thread ID. */
    private static AbstractComposeFragment fromConversation(Context context, long threadId, boolean group,
            boolean creatingGroup) {
        AbstractComposeFragment f = group ? new GroupMessageFragment() : new ComposeMessageFragment();
        Bundle args = new Bundle();
        args.putString("action", ComposeMessage.ACTION_VIEW_CONVERSATION);
        args.putParcelable("data", ContentUris.withAppendedId(Conversations.CONTENT_URI, threadId));
        args.putBoolean(ComposeMessage.EXTRA_CREATING_GROUP, creatingGroup);
        f.setArguments(args);
        return f;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // setListAdapter() is post-poned

        ListView list = getListView();

        setMultiChoiceModeListener(this);

        // add header view (this must be done before setting the adapter)
        mHeaderView = LayoutInflater.from(getActivity()).inflate(R.layout.message_list_header, list, false);
        mNextPageButton = mHeaderView.findViewById(R.id.load_next_page);
        mNextPageButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // disable button in the meantime
                enableHeaderView(false);
                // start query for the next page
                startMessagesQuery(mQueryHandler.getLastId());
            }
        });
        list.addHeaderView(mHeaderView, null, false);

        // set custom background (if any)
        ImageView background = (ImageView) getView().findViewById(R.id.background);
        Drawable bg = Preferences.getConversationBackground(getActivity());
        if (bg != null) {
            background.setScaleType(ImageView.ScaleType.CENTER_CROP);
            background.setImageDrawable(bg);
        } else {
            background.setScaleType(ImageView.ScaleType.FIT_XY);
            background.setImageResource(R.drawable.app_background_tile);
        }

        processArguments(savedInstanceState);
        initAttachmentView();
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        mLocalBroadcastManager = LocalBroadcastManager.getInstance(context);
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mLocalBroadcastManager = null;
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        mComposer.onKeyboardStateChanged(newConfig.keyboardHidden == KEYBOARDHIDDEN_NO);
    }

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

        mComposer = (ComposerBar) view.findViewById(R.id.composer_bar);
        mComposer.setComposerListener(this);

        // footer (for tablet presence status)
        mStatusText = (TextView) view.findViewById(R.id.status_text);

        mComposer.setRootView(view);

        Configuration config = getResources().getConfiguration();
        mComposer.onKeyboardStateChanged(config.keyboardHidden == KEYBOARDHIDDEN_NO);

        return view;
    }

    private final MessageListAdapter.OnContentChangedListener mContentChangedListener = new MessageListAdapter.OnContentChangedListener() {
        public void onContentChanged(MessageListAdapter adapter) {
            if (isVisible())
                startQuery();
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setHasOptionsMenu(true);
        mQueryHandler = new MessageListQueryHandler(this);
        mHandler = new Handler();

        // list adapter creation is post-poned
    }

    public boolean isActionModeActive() {
        return mCheckedItemCount > 0;
    }

    @Override
    public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
        if (checked)
            mCheckedItemCount++;
        else
            mCheckedItemCount--;
        mode.setTitle(
                getResources().getQuantityString(R.plurals.context_selected, mCheckedItemCount, mCheckedItemCount));

        mode.invalidate();
    }

    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        MenuInflater inflater = mode.getMenuInflater();
        inflater.inflate(R.menu.compose_message_ctx, menu);
        return true;
    }

    @Override
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
        MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
        MenuItem retryMenu = menu.findItem(R.id.menu_retry);
        MenuItem shareMenu = menu.findItem(R.id.menu_share);
        MenuItem copyTextMenu = menu.findItem(R.id.menu_copy_text);
        MenuItem detailsMenu = menu.findItem(R.id.menu_details);
        MenuItem openMenu = menu.findItem(R.id.menu_open);
        MenuItem dlMenu = menu.findItem(R.id.menu_download);
        MenuItem cancelDlMenu = menu.findItem(R.id.menu_cancel_download);

        // initial status
        deleteMenu.setVisible(true);
        retryMenu.setVisible(false);
        shareMenu.setVisible(false);
        copyTextMenu.setVisible(false);
        detailsMenu.setVisible(false);
        openMenu.setVisible(false);
        dlMenu.setVisible(false);
        cancelDlMenu.setVisible(false);

        boolean singleItem = (mCheckedItemCount == 1);
        if (singleItem) {
            CompositeMessage msg = getCheckedItem();

            // group command can't be deleted or have details
            if (msg.hasComponent(GroupCommandComponent.class)) {
                deleteMenu.setVisible(false);
            } else {
                // message waiting for user review or not delivered
                if (msg.getStatus() == Messages.STATUS_PENDING || msg.getStatus() == Messages.STATUS_NOTDELIVERED) {
                    retryMenu.setVisible(true);
                }

                // some commands can be used only on unencrypted messages
                if (!msg.isEncrypted()) {
                    AttachmentComponent attachment = msg.getComponent(AttachmentComponent.class);
                    TextComponent text = msg.getComponent(TextComponent.class);

                    // sharing media messages has no purpose if media file hasn't been
                    // retrieved yet
                    if (text != null || attachment == null || attachment.getLocalUri() != null)
                        shareMenu.setVisible(true);

                    // non-empty text: copy text to clipboard
                    if (text != null && !TextUtils.isEmpty(text.getContent()))
                        copyTextMenu.setVisible(true);

                    if (attachment != null) {

                        // message has a local uri - add open file entry
                        if (attachment.getLocalUri() != null) {
                            int resId;
                            if (attachment instanceof ImageComponent)
                                resId = R.string.view_image;
                            else if (attachment instanceof AudioComponent)
                                resId = R.string.open_audio;
                            else
                                resId = R.string.open_file;

                            openMenu.setTitle(resId);
                            openMenu.setVisible(true);
                        }

                        // message has a fetch url - add download control entry
                        if (msg.getDirection() == Messages.DIRECTION_IN && attachment.getFetchUrl() != null) {
                            if (!DownloadService.isQueued(attachment.getFetchUrl())) {
                                int string;
                                // already fetched
                                if (attachment.getLocalUri() != null)
                                    string = R.string.download_again;
                                else
                                    string = R.string.download_file;

                                dlMenu.setTitle(string);
                                dlMenu.setVisible(true);
                            } else {
                                cancelDlMenu.setVisible(true);
                            }
                        }
                    }
                }

                detailsMenu.setVisible(true);
            }
        }
        return true;
    }

    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        switch (item.getItemId()) {
        case R.id.menu_delete: {
            // using clone because listview returns its original copy
            deleteSelectedMessages(SystemUtils.cloneSparseBooleanArray(getListView().getCheckedItemPositions()));
            mode.finish();
            return true;
        }

        case R.id.menu_retry: {
            CompositeMessage msg = getCheckedItem();
            retryMessage(msg);
            mode.finish();
            return true;
        }

        case R.id.menu_share: {
            CompositeMessage msg = getCheckedItem();
            shareMessage(msg);
            mode.finish();
            return true;
        }

        case R.id.menu_copy_text: {
            CompositeMessage msg = getCheckedItem();

            TextComponent txt = msg.getComponent(TextComponent.class);

            String text = (txt != null) ? txt.getContent() : "";

            ClipboardManager cpm = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
            cpm.setText(text);

            Toast.makeText(getActivity(), R.string.message_text_copied, Toast.LENGTH_SHORT).show();
            mode.finish();
            return true;
        }

        case R.id.menu_open: {
            CompositeMessage msg = getCheckedItem();
            openFile(msg);
            mode.finish();
            return true;
        }

        case R.id.menu_download: {
            CompositeMessage msg = getCheckedItem();
            startDownload(msg);
            mode.finish();
            return true;
        }

        case R.id.menu_cancel_download: {
            CompositeMessage msg = getCheckedItem();
            stopDownload(msg);
            mode.finish();
            return true;
        }

        case R.id.menu_details: {
            CompositeMessage msg = getCheckedItem();
            showMessageDetails(msg);
            mode.finish();
            return true;
        }
        }
        return false;
    }

    @Override
    public void onDestroyActionMode(ActionMode mode) {
        mCheckedItemCount = 0;
        getListView().clearChoices();
        mListAdapter.notifyDataSetChanged();
    }

    private CompositeMessage getCheckedItem() {
        if (mCheckedItemCount != 1)
            throw new IllegalStateException("checked items count must be exactly 1");

        Cursor cursor = (Cursor) getListView().getItemAtPosition(getCheckedItemPosition());
        return CompositeMessage.fromCursor(getActivity(), cursor);
    }

    private int getCheckedItemPosition() {
        SparseBooleanArray checked = getListView().getCheckedItemPositions();
        return checked.keyAt(checked.indexOfValue(true));
    }

    private void deleteSelectedMessages(final SparseBooleanArray checked) {
        new MaterialDialog.Builder(getActivity()).content(R.string.confirm_will_delete_messages)
                .positiveText(android.R.string.ok).positiveColorRes(R.color.button_danger)
                .onPositive(new MaterialDialog.SingleButtonCallback() {
                    @Override
                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                        Context ctx = getActivity();
                        for (int i = 0, c = getListView().getCount()
                                + getListView().getHeaderViewsCount(); i < c; ++i) {
                            if (checked.get(i)) {
                                Cursor cursor = (Cursor) getListView().getItemAtPosition(i);
                                // skip group command messages
                                if (!GroupCommandComponent.isCursor(cursor))
                                    CompositeMessage.deleteFromCursor(ctx, cursor);
                            }
                        }
                        mListAdapter.notifyDataSetChanged();
                    }
                }).negativeText(android.R.string.cancel).show();
    }

    private void initAttachmentView() {
        View view = getView();

        mAttachmentContainer = (AttachmentRevealFrameLayout) view.findViewById(R.id.attachment_container);

        View.OnClickListener hideAttachmentListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                hideAttachmentView();
            }
        };
        view.findViewById(R.id.attachment_overlay).setOnClickListener(hideAttachmentListener);
        view.findViewById(R.id.attach_hide).setOnClickListener(hideAttachmentListener);

        view.findViewById(R.id.attach_camera).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                selectPhotoAttachment();
                hideAttachmentView();
            }
        });

        view.findViewById(R.id.attach_gallery).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                selectGalleryAttachment();
                hideAttachmentView();
            }
        });

        view.findViewById(R.id.attach_video).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getContext(), R.string.msg_not_implemented, Toast.LENGTH_SHORT).show();
            }
        });

        view.findViewById(R.id.attach_audio).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                selectAudioAttachment();
                hideAttachmentView();
            }
        });

        view.findViewById(R.id.attach_file).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getContext(), R.string.msg_not_implemented, Toast.LENGTH_SHORT).show();
            }
        });

        view.findViewById(R.id.attach_vcard).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                selectContactAttachment();
                hideAttachmentView();
            }
        });

        view.findViewById(R.id.attach_location).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getContext(), R.string.msg_not_implemented, Toast.LENGTH_SHORT).show();
            }
        });
    }

    /** Sends out a binary message. */
    @Override
    public void sendBinaryMessage(Uri uri, String mime, boolean media, Class<? extends MessageComponent<?>> klass) {
        Log.v(TAG, "sending binary content: " + uri);

        try {
            // TODO convert to thread (?)

            offlineModeWarning();

            final Context context = getContext();
            final Conversation conv = mConversation;
            Uri newMsg = Kontalk.getMessagesController(context).sendBinaryMessage(conv, uri, mime, media, klass);

            // update thread id from the inserted message
            if (threadId <= 0) {
                threadId = MessagesProviderUtils.getThreadByMessage(getContext(), newMsg);
                if (threadId > 0) {
                    // we can run it here because progress=false
                    startQuery();
                } else {
                    Log.v(TAG, "no data - cannot start query for this composer");
                }
            }
        } catch (SQLiteDiskIOException e) {
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(getActivity(), R.string.error_store_outbox, Toast.LENGTH_LONG).show();
                }
            });
        } catch (Exception e) {
            ReportingManager.logException(e);
            getActivity().runOnUiThread(new Runnable() {
                public void run() {
                    Toast.makeText(getActivity(), R.string.err_store_message_failed, Toast.LENGTH_LONG).show();
                }
            });
        }
    }

    private final class TextMessageThread extends Thread {
        private final String mText;

        TextMessageThread(String text) {
            mText = text;
        }

        @Override
        public void run() {
            try {
                final Context context = getContext();
                final Conversation conv = mConversation;
                Uri newMsg = Kontalk.getMessagesController(context).sendTextMessage(conv, mText);

                // update thread id from the inserted message
                if (threadId <= 0) {
                    threadId = MessagesProviderUtils.getThreadByMessage(context, newMsg);
                    if (threadId > 0) {
                        startQuery();
                    } else {
                        Log.v(TAG, "no data - cannot start query for this composer");
                    }
                }
            } catch (SQLiteDiskIOException e) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(getActivity(), R.string.error_store_outbox, Toast.LENGTH_LONG).show();
                    }
                });
            } catch (Exception e) {
                ReportingManager.logException(e);
                getActivity().runOnUiThread(new Runnable() {
                    public void run() {
                        Toast.makeText(getActivity(), R.string.err_store_message_failed, Toast.LENGTH_LONG).show();
                    }
                });
            }
        }
    }

    /** Sends out the text message in the composing entry. */
    @Override
    public void sendTextMessage(String message) {
        if (!TextUtils.isEmpty(message)) {
            offlineModeWarning();

            // start thread
            new TextMessageThread(message).start();
        }
    }

    /** Sends an inactive chat state message. */
    public abstract boolean sendInactive();

    protected abstract void onInflateOptionsMenu(Menu menu, MenuInflater inflater);

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        onInflateOptionsMenu(menu, inflater);
        mDeleteThreadMenu = menu.findItem(R.id.delete_thread);
        mToggleEncryptionMenu = menu.findItem(R.id.toggle_encryption);
        updateUI();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // action mode is active - no processing
        if (isActionModeActive())
            return true;

        switch (item.getItemId()) {
        case R.id.menu_attachment:
            toggleAttachmentView();
            return true;

        case R.id.delete_thread:
            if (threadId > 0)
                deleteThread();
            return true;

        case R.id.invite_group:
            addUsers();
            return true;

        case R.id.toggle_encryption:
            toggleEncryption();
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void toggleEncryption() {
        if (mConversation.isEncryptionEnabled()) {
            new MaterialDialog.Builder(getActivity()).title(R.string.title_disable_encryption)
                    .content(R.string.msg_disable_encryption).positiveText(R.string.menu_disable_encryption)
                    .positiveColorRes(R.color.button_danger).negativeText(android.R.string.cancel)
                    .onPositive(new MaterialDialog.SingleButtonCallback() {
                        @Override
                        public void onClick(@NonNull MaterialDialog materialDialog,
                                @NonNull DialogAction dialogAction) {
                            setEncryption(false);
                            updateUI();
                        }
                    }).show();
        } else {
            setEncryption(true);
            updateUI();
        }
    }

    void setEncryption(boolean encryption) {
        if (mConversation != null)
            mConversation.setEncryptionEnabled(encryption);
    }

    @Override
    public void onListItemClick(ListView listView, View view, int position, long id) {
        int choiceMode = listView.getChoiceMode();
        if (choiceMode == ListView.CHOICE_MODE_NONE || choiceMode == ListView.CHOICE_MODE_SINGLE) {
            MessageListItem item = (MessageListItem) view;
            final CompositeMessage msg = item.getMessage();

            AttachmentComponent attachment = msg.getComponent(AttachmentComponent.class);

            if (attachment != null && (attachment.getFetchUrl() != null || attachment.getLocalUri() != null)) {

                // outgoing message or already fetched
                if (attachment.getLocalUri() != null) {
                    // open file
                    openFile(msg);
                } else {
                    // info & download dialog
                    CharSequence message = MessageUtils.getFileInfoMessage(getActivity(), msg, getDecodedPeer(msg));

                    MaterialDialog.Builder builder = new MaterialDialog.Builder(getActivity())
                            .title(R.string.title_file_info).content(message).negativeText(android.R.string.cancel)
                            .cancelable(true);

                    if (!DownloadService.isQueued(attachment.getFetchUrl())) {
                        MaterialDialog.SingleButtonCallback startDL = new MaterialDialog.SingleButtonCallback() {
                            @Override
                            public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                                // start file download
                                startDownload(msg);
                            }
                        };
                        builder.positiveText(R.string.download).onPositive(startDL);
                    } else {
                        MaterialDialog.SingleButtonCallback stopDL = new MaterialDialog.SingleButtonCallback() {
                            @Override
                            public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                                // cancel file download
                                stopDownload(msg);
                            }
                        };
                        builder.positiveText(R.string.download_cancel).onPositive(stopDL);
                    }

                    builder.show();
                }
            }

            else {
                item.onClick();
            }
        } else {
            super.onListItemClick(listView, view, position, id);
        }
    }

    private void startDownload(CompositeMessage msg) {
        AttachmentComponent attachment = msg.getComponent(AttachmentComponent.class);

        if (attachment != null && attachment.getFetchUrl() != null) {
            DownloadService.start(getContext(), msg.getDatabaseId(), msg.getSender(), attachment.getMime(),
                    msg.getTimestamp(), attachment.getSecurityFlags() != Coder.SECURITY_CLEARTEXT,
                    attachment.getFetchUrl());
        } else {
            // corrupted message :(
            Toast.makeText(getActivity(), R.string.err_attachment_corrupted, Toast.LENGTH_LONG).show();
        }
    }

    private void stopDownload(CompositeMessage msg) {
        AttachmentComponent attachment = msg.getComponent(AttachmentComponent.class);

        if (attachment != null && attachment.getFetchUrl() != null) {
            DownloadService.abort(getContext(), Uri.parse(attachment.getFetchUrl()));
        }
    }

    private void openFile(CompositeMessage msg) {
        AttachmentComponent attachment = msg.getComponent(AttachmentComponent.class);

        if (attachment != null) {
            Intent i = new Intent(Intent.ACTION_VIEW);
            i.setDataAndType(attachment.getLocalUri(), attachment.getMime());
            try {
                startActivity(i);
            } catch (ActivityNotFoundException e) {
                Toast.makeText(getActivity(), R.string.chooser_error_no_app, Toast.LENGTH_LONG).show();
            }
        }
    }

    private void chooseContact() {
        // TODO one day it will be like this
        // Intent i = new Intent(Intent.ACTION_PICK, Users.CONTENT_URI);
        Intent i = new Intent(getContext(), ContactsListActivity.class);
        i.putExtra(ContactsListActivity.MODE_MULTI_SELECT, true);
        i.putExtra(ContactsListActivity.MODE_ADD_USERS, true);
        startActivityForResult(i, REQUEST_INVITE_USERS);
    }

    boolean tryHideAttachmentView() {
        return tryHideAttachmentView(false);
    }

    boolean tryHideAttachmentView(boolean instant) {
        if (isAttachmentViewVisible()) {
            mAttachmentContainer.hide(instant);
            return true;
        }
        return false;
    }

    private boolean isAttachmentViewVisible() {
        return mAttachmentContainer.getVisibility() == View.VISIBLE && !mAttachmentContainer.isClosing();
    }

    void hideAttachmentView() {
        mAttachmentContainer.hide();
    }

    /** Show or hide the attachment selector. */
    private void toggleAttachmentView() {
        mComposer.forceHideKeyboard();
        mAttachmentContainer.toggle();
    }

    /** Starts an activity for shooting a picture. */
    void selectPhotoAttachment() {
        try {
            // check if camera is available
            final PackageManager packageManager = getActivity().getPackageManager();
            final Intent intent = SystemUtils.externalIntent(MediaStore.ACTION_IMAGE_CAPTURE);
            List<ResolveInfo> list = packageManager.queryIntentActivities(intent,
                    PackageManager.MATCH_DEFAULT_ONLY);
            if (list.size() <= 0)
                throw new UnsupportedOperationException();

            mCurrentPhoto = MediaStorage.getOutgoingPhotoFile();
            Uri uri = Uri.fromFile(mCurrentPhoto);
            Intent take = SystemUtils.externalIntent(MediaStore.ACTION_IMAGE_CAPTURE);
            take.putExtra(MediaStore.EXTRA_OUTPUT, uri);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                take.setClipData(ClipData.newUri(getContext().getContentResolver(), "Picture path", uri));
            }

            startActivityForResult(take, SELECT_ATTACHMENT_PHOTO);
        } catch (UnsupportedOperationException ue) {
            Toast.makeText(getActivity(), R.string.chooser_error_no_camera_app, Toast.LENGTH_LONG).show();
        } catch (IOException e) {
            Log.e(TAG, "error creating temp file", e);
            Toast.makeText(getActivity(), R.string.chooser_error_no_camera, Toast.LENGTH_LONG).show();
        }
    }

    /** Starts an activity for picture attachment selection. */
    @TargetApi(Build.VERSION_CODES.KITKAT)
    void selectGalleryAttachment() {
        boolean useSAF = MediaStorage.isStorageAccessFrameworkAvailable();
        Intent pictureIntent = createGalleryIntent(useSAF);

        try {
            startActivityForResult(pictureIntent, SELECT_ATTACHMENT_OPENABLE);
        } catch (ActivityNotFoundException e1) {
            try {
                if (useSAF) {
                    // try direct file system access
                    pictureIntent = createGalleryIntent(false);
                    startActivityForResult(pictureIntent, SELECT_ATTACHMENT_OPENABLE);
                } else {
                    // simulate error
                    throw new ActivityNotFoundException("gallery");
                }
            } catch (ActivityNotFoundException e2) {
                Toast.makeText(getActivity(), R.string.chooser_error_no_gallery_app, Toast.LENGTH_LONG).show();
            }
        }
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    private Intent createGalleryIntent(boolean useSAF) {
        Intent intent;
        if (!useSAF) {
            intent = SystemUtils.externalIntent(Intent.ACTION_GET_CONTENT)
                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        } else {
            intent = SystemUtils.externalIntent(Intent.ACTION_OPEN_DOCUMENT);
        }

        return intent.addCategory(Intent.CATEGORY_OPENABLE).setType("image/*")
                .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
    }

    /** Starts activity for a vCard attachment from a contact. */
    void selectContactAttachment() {
        try {
            Intent i = SystemUtils.externalIntent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
            startActivityForResult(i, SELECT_ATTACHMENT_CONTACT);
        } catch (ActivityNotFoundException e) {
            // no contacts app found (crap device eh?)
            Toast.makeText(getActivity(), R.string.err_no_contacts_app, Toast.LENGTH_LONG).show();
        }
    }

    void selectAudioAttachment() {
        // create audio fragment if needed
        AudioFragment audio = getAudioFragment();
        // stop everything
        if (mAudioControl != null) {
            resetAudio(mAudioControl);
        } else {
            audio.resetPlayer();
            audio.setMessageId(-1);
        }
        // show dialog
        mAudioDialog = new AudioDialog(getActivity(), audio, this);
        mAudioDialog.show();
    }

    private AudioFragment getAudioFragment() {
        FragmentManager fm = getFragmentManager();
        if (fm != null) {
            AudioFragment found = (AudioFragment) fm.findFragmentByTag("audio");
            if (found != null) {
                mAudioFragment = found;
            } else {
                mAudioFragment = new AudioFragment();
                fm.beginTransaction().add(mAudioFragment, "audio").commit();
            }
        }

        return mAudioFragment;
    }

    protected abstract void deleteConversation();

    private void deleteThread() {
        new MaterialDialog.Builder(getActivity()).content(R.string.confirm_will_delete_thread)
                .positiveText(android.R.string.ok).positiveColorRes(R.color.button_danger)
                .negativeText(android.R.string.cancel).onPositive(new MaterialDialog.SingleButtonCallback() {
                    @Override
                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                        deleteConversation();
                    }
                }).show();
    }

    void addUsers() {
        chooseContact();
    }

    protected abstract void addUsers(String[] members);

    private void retryMessage(CompositeMessage msg) {
        MessageCenterService.retryMessage(getContext(),
                ContentUris.withAppendedId(Messages.CONTENT_URI, msg.getDatabaseId()),
                mConversation.isEncryptionEnabled());
    }

    void scrollToPosition(int position) {
        getListView().setSelection(position);
    }

    private boolean isSearching() {
        Bundle args = getArguments();
        return args != null && args.getLong(ComposeMessage.EXTRA_MESSAGE, -1) >= 0;
    }

    protected synchronized void startQuery() {
        Conversation.startQuery(mQueryHandler, CONVERSATION_QUERY_TOKEN, threadId);
        // message list query will be started by query handler
    }

    void startMessagesQuery() {
        CompositeMessage.startQuery(mQueryHandler, MESSAGE_LIST_QUERY_TOKEN, threadId,
                isSearching() ? 0 : MESSAGE_PAGE_SIZE, 0);
    }

    void startMessagesQuery(long lastId) {
        CompositeMessage.startQuery(mQueryHandler, MESSAGE_PAGE_QUERY_TOKEN, threadId,
                isSearching() ? 0 : MESSAGE_PAGE_SIZE, lastId);
    }

    private void stopQuery() {
        hideHeaderView();
        if (mListAdapter != null)
            mListAdapter.changeCursor(null);

        if (mQueryHandler != null) {
            // be sure to cancel all queries
            mQueryHandler.abort();
        }
    }

    private void showMessageDetails(CompositeMessage msg) {
        MessageUtils.showMessageDetails(getActivity(), msg, getDecodedPeer(msg), getDecodedName(msg));
    }

    /** Returns the phone number of the message sender, if available. */
    protected abstract String getDecodedPeer(CompositeMessage msg);

    /** Returns the display name of the message sender, if available. */
    protected abstract String getDecodedName(CompositeMessage msg);

    private void shareMessage(CompositeMessage msg) {
        Intent i = null;
        AttachmentComponent attachment = msg.getComponent(AttachmentComponent.class);

        if (attachment != null) {
            i = ComposeMessage.sendMediaMessage(attachment.getLocalUri(), attachment.getMime());
        }

        else {
            TextComponent txt = msg.getComponent(TextComponent.class);

            if (txt != null)
                i = ComposeMessage.sendTextMessage(txt.getContent());
        }

        if (i != null)
            startActivity(i);
        else
            // TODO ehm...
            Log.w(TAG, "error sharing message");
    }

    protected void loadConversationMetadata(Uri uri) {
        threadId = ContentUris.parseId(uri);
        mConversation = Conversation.loadFromId(getActivity(), threadId);
        if (mConversation == null) {
            Log.w(TAG, "conversation for thread " + threadId + " not found!");
            startActivity(new Intent(getActivity(), ConversationsActivity.class));
            getActivity().finish();
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        // image from storage/picture from camera
        // since there are like up to 3 different ways of doing this...
        if (requestCode == SELECT_ATTACHMENT_OPENABLE || requestCode == SELECT_ATTACHMENT_PHOTO) {
            if (resultCode == Activity.RESULT_OK) {
                Uri[] uris = null;
                String[] mimes = null;

                // returning from camera
                if (requestCode == SELECT_ATTACHMENT_PHOTO) {
                    if (mCurrentPhoto != null) {
                        Uri uri = Uri.fromFile(mCurrentPhoto);
                        // notify media scanner
                        Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
                        mediaScanIntent.setData(uri);
                        getActivity().sendBroadcast(mediaScanIntent);
                        mCurrentPhoto = null;

                        uris = new Uri[] { uri };
                    }
                } else {
                    if (mCurrentPhoto != null) {
                        mCurrentPhoto.delete();
                        mCurrentPhoto = null;
                    }

                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && data.getClipData() != null) {
                        ClipData cdata = data.getClipData();
                        uris = new Uri[cdata.getItemCount()];

                        for (int i = 0; i < uris.length; i++) {
                            ClipData.Item item = cdata.getItemAt(i);
                            uris[i] = item.getUri();
                        }
                    } else {
                        uris = new Uri[] { data.getData() };
                        mimes = new String[] { data.getType() };
                    }

                    // SAF available, request persistable permissions
                    if (MediaStorage.isStorageAccessFrameworkAvailable()) {
                        for (Uri uri : uris) {
                            if (uri != null && !"file".equals(uri.getScheme())) {
                                MediaStorage.requestPersistablePermissions(getActivity(), uri);
                            }
                        }
                    }
                }

                for (int i = 0; uris != null && i < uris.length; i++) {
                    Uri uri = uris[i];
                    if (uri == null)
                        continue;

                    String mime = (mimes != null && mimes.length >= uris.length) ? mimes[i] : null;

                    if (mime == null || mime.startsWith("*/") || mime.endsWith("/*")) {
                        mime = MediaStorage.getType(getActivity(), uri);
                        Log.v(TAG, "using detected mime type " + mime);
                    }

                    if (ImageComponent.supportsMimeType(mime))
                        sendBinaryMessage(uri, mime, true, ImageComponent.class);
                    else if (VCardComponent.supportsMimeType(mime))
                        sendBinaryMessage(uri, VCardComponent.MIME_TYPE, false, VCardComponent.class);
                    else
                        Toast.makeText(getActivity(), R.string.send_mime_not_supported, Toast.LENGTH_LONG).show();
                }
            }
            // operation aborted
            else {
                // delete photo :)
                if (mCurrentPhoto != null) {
                    mCurrentPhoto.delete();
                    mCurrentPhoto = null;
                }
            }
        }
        // contact card (vCard)
        else if (requestCode == SELECT_ATTACHMENT_CONTACT) {
            if (resultCode == Activity.RESULT_OK) {
                Uri uri = data.getData();
                if (uri != null) {
                    Uri vcardUri = null;

                    // get lookup key
                    final Cursor c = getContext().getContentResolver().query(uri,
                            new String[] { Contacts.LOOKUP_KEY }, null, null, null);
                    if (c != null) {
                        try {
                            if (c.moveToFirst()) {
                                String lookupKey = c.getString(0);
                                vcardUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
                            }
                        } catch (Exception e) {
                            Log.w(TAG, "unable to lookup selected contact. Did you grant me the permission?", e);
                            ReportingManager.logException(e);
                        } finally {
                            c.close();
                        }
                    }

                    if (vcardUri != null) {
                        sendBinaryMessage(vcardUri, VCardComponent.MIME_TYPE, false, VCardComponent.class);
                    } else {
                        Toast.makeText(getContext(), R.string.err_no_contact, Toast.LENGTH_LONG).show();
                    }
                }
            }
        }
        // invite user
        else if (requestCode == REQUEST_INVITE_USERS) {
            if (resultCode == Activity.RESULT_OK) {

                ArrayList<Uri> uris;
                Uri threadUri = data.getData();
                if (threadUri != null) {
                    String userId = threadUri.getLastPathSegment();
                    addUsers(new String[] { userId });
                } else if ((uris = data.getParcelableArrayListExtra("org.kontalk.contacts")) != null) {
                    String[] users = new String[uris.size()];
                    for (int i = 0; i < users.length; i++)
                        users[i] = uris.get(i).getLastPathSegment();
                    addUsers(users);
                }

            }
        }
    }

    @Override
    public void onSaveInstanceState(Bundle out) {
        super.onSaveInstanceState(out);
        out.putParcelable(Uri.class.getName(), Threads.getUri(getUserId()));
        // save composer status
        if (mComposer != null)
            mComposer.onSaveInstanceState(out);
        // current photo being shot
        if (mCurrentPhoto != null) {
            out.putString("currentPhoto", mCurrentPhoto.toString());
        }
        // audio dialog open
        if (mAudioDialog != null) {
            mAudioDialog.onSaveInstanceState(out);
        }
        // audio player stuff
        out.putInt("mediaPlayerStatus", mMediaPlayerStatus);
    }

    /** Handles ACTION_VIEW intents. */
    protected abstract void handleActionView(Uri uri);

    /** Handles ACTION_VIEW_USERID intents: providing the user ID/JID. */
    protected abstract boolean handleActionViewConversation(Uri uri, Bundle args);

    private void processArguments(Bundle savedInstanceState) {
        Bundle args;
        if (savedInstanceState != null) {
            Uri uri = savedInstanceState.getParcelable(Uri.class.getName());
            // threadId = ContentUris.parseId(uri);
            args = new Bundle();
            args.putString("action", ComposeMessage.ACTION_VIEW_USERID);
            args.putParcelable("data", uri);

            String currentPhoto = savedInstanceState.getString("currentPhoto");
            if (currentPhoto != null) {
                mCurrentPhoto = new File(currentPhoto);
            }

            // audio playing
            setAudioStatus(savedInstanceState.getInt("mediaPlayerStatus", AudioContentView.STATUS_IDLE));

            // audio dialog stuff
            mAudioDialog = AudioDialog.onRestoreInstanceState(getActivity(), savedInstanceState, getAudioFragment(),
                    this);
            if (mAudioDialog != null) {
                Log.d(TAG, "recreating audio dialog");
                mAudioDialog.show();
            }
        } else {
            args = getArguments();
        }

        if (args != null && args.size() > 0) {
            final String action = args.getString("action");

            // view intent
            if (Intent.ACTION_VIEW.equals(action)) {
                Uri uri = args.getParcelable("data");
                handleActionView(uri);
            }

            // view conversation - just threadId provided
            else if (ComposeMessage.ACTION_VIEW_CONVERSATION.equals(action)) {
                Uri uri = args.getParcelable("data");
                loadConversationMetadata(uri);
            }

            // view conversation - just userId provided
            else if (ComposeMessage.ACTION_VIEW_USERID.equals(action)) {
                Uri uri = args.getParcelable("data");
                if (!handleActionViewConversation(uri, args)) {
                    getActivity().finish();
                    return;
                }
            }
        }

        // set title if we are autonomous
        if (args != null) {
            String title = mUserName;
            //if (mUserPhone != null) title += " <" + mUserPhone + ">";
            setActivityTitle(title, "");
        }

        // update conversation stuff
        if (mConversation != null)
            onConversationCreated();

        onArgumentsProcessed();
    }

    protected abstract void onArgumentsProcessed();

    public void setActivityTitle(CharSequence title, CharSequence status) {
        if (mStatusText != null) {
            // tablet UI - ignore title
            mStatusText.setText(status);
        } else {
            ComposeMessageParent parent = (ComposeMessageParent) getActivity();
            parent.setTitle(title, status);
        }
    }

    public void setActivityStatusUpdating() {
        if (mStatusText != null) {
            CharSequence text = mStatusText.getText();
            if (text != null && text.length() > 0) {
                mStatusText.setText(ComposeMessage.applyUpdatingStyle(text));
            }
        } else {
            ComposeMessageParent parent = (ComposeMessageParent) getActivity();
            parent.setUpdatingSubtitle();
        }
    }

    public ComposeMessage getParentActivity() {
        Activity _activity = getActivity();
        return (_activity instanceof ComposeMessage) ? (ComposeMessage) _activity : null;
    }

    void processStart() {
        ComposeMessage activity = getParentActivity();
        // opening for contact picker - do nothing
        if (threadId < 0 && activity != null && activity.getSendIntent() != null)
            return;

        if (mListAdapter == null) {
            Pattern highlight = null;
            Bundle args = getArguments();
            if (args != null) {
                String highlightString = args.getString(ComposeMessage.EXTRA_HIGHLIGHT);
                highlight = (highlightString == null) ? null
                        : Pattern.compile("\\b" + Pattern.quote(highlightString), Pattern.CASE_INSENSITIVE);
            }

            mListAdapter = new MessageListAdapter(getActivity(), null, highlight, getListView(), this);
            mListAdapter.setOnContentChangedListener(mContentChangedListener);
            setListAdapter(mListAdapter);
        }

        if (threadId > 0) {
            // always reload conversation
            startQuery();
        } else {
            mConversation = Conversation.createNew(getActivity());
            mConversation.setRecipient(getUserId());
            onConversationCreated();
        }
    }

    /** Called when the {@link Conversation} object has been created. */
    protected void onConversationCreated() {
        // restore any draft
        mComposer.restoreText(mConversation.getDraft());

        if (mConversation.getThreadId() > 0) {
            if (mConversation.getUnreadCount() > 0) {
                /*
                 * FIXME this has the usual issue about resuming while screen is
                 * still locked, having focus and so on...
                 * See issue #28.
                 */
                Log.v(TAG, "marking thread as read");
                mConversation.markAsRead();
            }
        } else {
            // new conversation -- observe peer Uri
            registerPeerObserver();
        }

        // subscribe to presence notifications
        subscribePresence();

        updateUI();
    }

    /** Called when a presence is received. */
    protected abstract void onPresence(String jid, Presence.Type type, boolean removed, Presence.Mode mode,
            String fingerprint);

    protected abstract void onConnected();

    /** Called when the roster has been loaded (ACTION_ROSTER). */
    protected abstract void onRosterLoaded();

    /** Called when the contact starts typing. */
    protected abstract void onStartTyping(String jid);

    /** Called when the contact stops typing. */
    protected abstract void onStopTyping(String jid);

    /** Should return true if the contact is a user ID in the current context. */
    protected abstract boolean isUserId(String jid);

    private void subscribePresence() {
        // TODO this needs serious refactoring
        if (mPresenceReceiver == null) {
            mPresenceReceiver = new BroadcastReceiver() {
                public void onReceive(Context context, Intent intent) {
                    // activity is terminating
                    if (getContext() == null)
                        return;

                    String action = intent.getAction();

                    if (MessageCenterService.ACTION_PRESENCE.equals(action)) {
                        String from = intent.getStringExtra(MessageCenterService.EXTRA_FROM);
                        String bareFrom = from != null ? XmppStringUtils.parseBareJid(from) : null;

                        // we are receiving a presence from our peer
                        if (from != null && isUserId(bareFrom)) {

                            // we handle only (un)available presence stanzas
                            String type = intent.getStringExtra(MessageCenterService.EXTRA_TYPE);
                            Presence.Type presenceType = (type != null) ? Presence.Type.fromString(type) : null;

                            String mode = intent.getStringExtra(MessageCenterService.EXTRA_SHOW);
                            Presence.Mode presenceMode = (mode != null) ? Presence.Mode.fromString(mode) : null;

                            String fingerprint = intent.getStringExtra(MessageCenterService.EXTRA_FINGERPRINT);

                            boolean removed = false;
                            if (presenceType == Presence.Type.available) {
                                mAvailableResources.add(from);
                            } else if (presenceType == Presence.Type.unavailable) {
                                removed = mAvailableResources.remove(from);
                            }

                            onPresence(from, presenceType, removed, presenceMode, fingerprint);
                        }
                    }

                    else if (MessageCenterService.ACTION_CONNECTED.equals(action)) {
                        // reset compose sent flag
                        mComposer.resetCompose();
                        // reset available resources list
                        mAvailableResources.clear();

                        onConnected();
                    }

                    else if (MessageCenterService.ACTION_ROSTER_LOADED.equals(action)) {
                        onRosterLoaded();
                    }

                    else if (MessageCenterService.ACTION_MESSAGE.equals(action)) {
                        String from = intent.getStringExtra(MessageCenterService.EXTRA_FROM);
                        String chatState = intent.getStringExtra("org.kontalk.message.chatState");

                        // we are receiving a composing notification from our peer
                        if (from != null && isUserId(from)) {
                            if (chatState != null && ChatState.composing.toString().equals(chatState)) {
                                onStartTyping(from);
                            } else {
                                onStopTyping(from);
                            }
                        }
                    }

                }
            };

            // listen for user presence, connection and incoming messages
            IntentFilter filter = new IntentFilter();
            filter.addAction(MessageCenterService.ACTION_PRESENCE);
            filter.addAction(MessageCenterService.ACTION_CONNECTED);
            filter.addAction(MessageCenterService.ACTION_ROSTER_LOADED);
            filter.addAction(MessageCenterService.ACTION_MESSAGE);

            mLocalBroadcastManager.registerReceiver(mPresenceReceiver, filter);

            // request connection and roster load status
            Context ctx = getActivity();
            if (ctx != null) {
                MessageCenterService.requestConnectionStatus(ctx);
                MessageCenterService.requestRosterStatus(ctx);
            }
        }
    }

    private void unsubscribePresence() {
        if (mPresenceReceiver != null) {
            mLocalBroadcastManager.unregisterReceiver(mPresenceReceiver);
            mPresenceReceiver = null;
        }
    }

    protected boolean isWarningVisible(WarningType type) {
        Snackbar bar = SnackbarManager.getCurrentSnackbar();
        if (bar != null) {
            WarningType oldType = (WarningType) bar.getTag();
            if (oldType != null && oldType == type)
                return true;
        }
        return false;
    }

    protected void hideWarning() {
        SnackbarManager.dismiss();
    }

    protected void showWarning(CharSequence text, final View.OnClickListener listener, WarningType type) {
        View view = getView();
        Activity context = getActivity();
        if (view == null || context == null)
            return;

        Snackbar bar = SnackbarManager.getCurrentSnackbar();
        if (bar != null) {
            WarningType oldType = (WarningType) bar.getTag();
            if (oldType != null && oldType.getValue() > type.getValue())
                return;

            bar.dismiss();
        }

        bar = Snackbar.with(context).type(SnackbarType.MULTI_LINE).text(text)
                .duration(Snackbar.SnackbarDuration.LENGTH_INDEFINITE).dismissOnActionClicked(false)
                .allowMultipleActionClicks(true);

        if (listener != null) {
            bar.swipeToDismiss(false).actionLabel(R.string.warning_button_details)
                    .actionListener(new ActionClickListener() {
                        @Override
                        public void onActionClicked(Snackbar snackbar) {
                            listener.onClick(null);
                        }
                    });
        } else {
            bar.swipeToDismiss(true).animation(false);
        }

        int colorId = 0;
        int textColorId = 0;
        switch (type) {
        case FATAL:
            textColorId = R.color.warning_bar_text_fatal;
            colorId = R.color.warning_bar_background_fatal;
            break;
        case WARNING:
            textColorId = R.color.warning_bar_text_warning;
            colorId = R.color.warning_bar_background_warning;
            break;
        }

        bar.setTag(type);
        bar.color(ContextCompat.getColor(context, colorId)).textColor(ContextCompat.getColor(context, textColorId));

        if (listener != null) {
            SnackbarManager.show(bar);
        } else {
            SnackbarManager.show(bar, (ViewGroup) view.findViewById(R.id.warning_bar));
        }
    }

    protected void setStatusText(CharSequence text) {
        ComposeMessageParent parent = (ComposeMessageParent) getActivity();
        if (parent instanceof ComposeMessage)
            setActivityTitle(null, text);
        else {
            if (mStatusText != null)
                mStatusText.setText(text);
        }
    }

    private synchronized void registerPeerObserver() {
        if (mPeerObserver == null) {
            Uri uri = Threads.getUri(mConversation.getRecipient());
            mPeerObserver = new PeerObserver(getActivity(), mQueryHandler);
            getActivity().getContentResolver().registerContentObserver(uri, false, mPeerObserver);
        }
    }

    synchronized void unregisterPeerObserver() {
        if (mPeerObserver != null) {
            Context context = mPeerObserver.mContext;
            context.getContentResolver().unregisterContentObserver(mPeerObserver);
            mPeerObserver = null;
        }
    }

    private final class PeerObserver extends ContentObserver {
        final Context mContext;

        PeerObserver(Context context, Handler handler) {
            super(handler);
            mContext = context;
        }

        @Override
        public void onChange(boolean selfChange) {
            Conversation conv = Conversation.loadFromUserId(mContext, getUserId());

            if (conv != null) {
                mConversation = conv;
                threadId = mConversation.getThreadId();

                // auto-unregister
                unregisterPeerObserver();
            }

            // fire cursor update
            Log.v(TAG, "peer observer active");
            processStart();
        }

        @Override
        public boolean deliverSelfNotifications() {
            return false;
        }
    }

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

        if (Authenticator.getDefaultAccount(getActivity()) == null) {
            NumberValidation.start(getActivity());
            getActivity().finish();
            return;
        }

        // hold message center
        MessageCenterService.hold(getActivity(), true);

        ComposeMessage activity = getParentActivity();
        if (activity == null || !activity.hasLostFocus() || activity.hasWindowFocus()) {
            onFocus();
        }
    }

    public void onFocus() {
        // resume content watcher
        resumeContentListener();

        // set notifications on pause
        MessagingNotification.setPaused(getUserId());

        // we are updating the status now
        setActivityStatusUpdating();

        // cursor was previously destroyed -- reload everything
        processStart();
    }

    @Override
    public void onPause() {
        super.onPause();

        // unsubcribe presence notifications
        unsubscribePresence();

        // notify composer bar
        mComposer.onPause();

        // hide attachment view
        tryHideAttachmentView(true);

        // hide emoji drawer
        tryHideEmojiDrawer();

        // pause content watcher
        pauseContentListener();

        // notify parent of pausing
        ComposeMessage parent = getParentActivity();
        if (parent != null)
            parent.fragmentLostFocus();

        CharSequence text = mComposer.getText();
        int len = text.length();

        // resume notifications
        MessagingNotification.setPaused(null);

        // save last message as draft
        if (threadId > 0) {

            // no draft and no messages - delete conversation
            if (len == 0 && mConversation.getMessageCount() == 0
                    && mConversation.getRequestStatus() != Threads.REQUEST_WAITING
                    && !mConversation.isGroupChat()) {

                mConversation.delete(false);
            }

            // update draft
            else {
                try {
                    MessagesProviderUtils.updateDraft(getContext(), threadId, text.toString());
                } catch (SQLiteDiskIOException e) {
                    // TODO warn user
                    Log.w(TAG, "error saving draft", e);
                    len = 0;
                }
            }
        }

        // new thread, create empty conversation
        else {
            if (len > 0) {
                // save to local storage
                try {
                    MessagesProviderUtils.insertEmptyThread(getActivity(), getUserId(), text.toString());
                } catch (SQLiteDiskIOException e) {
                    // TODO warn user
                    Log.w(TAG, "error saving draft", e);
                    len = 0;
                }
            }
        }

        if (len > 0) {
            Toast.makeText(getActivity(), R.string.msg_draft_saved, Toast.LENGTH_LONG).show();
        }

        if (mComposer.isComposeSent()) {
            // send inactive state notification
            sendInactive();
            mComposer.resetCompose();
        }

        // release message center
        MessageCenterService.release(getActivity());

        // release audio player
        AudioFragment audio = getAudioFragment();
        if (audio != null) {
            stopMediaPlayerUpdater();

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                if (!getActivity().isChangingConfigurations()) {
                    audio.setMessageId(-1);
                    audio.finish(true);
                }
            }
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        unregisterPeerObserver();
        stopQuery();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mComposer != null) {
            mComposer.onDestroy();
        }
        if (mAudioDialog != null) {
            mAudioDialog.dismiss();
            mAudioDialog = null;
        }
    }

    private void pauseContentListener() {
        if (mListAdapter != null)
            mListAdapter.setOnContentChangedListener(null);
    }

    private void resumeContentListener() {
        if (mListAdapter != null)
            mListAdapter.setOnContentChangedListener(mContentChangedListener);
    }

    public final boolean isFinishing() {
        Activity activity = getActivity();
        return (activity == null || activity.isFinishing()) || isRemoving();
    }

    void showHeaderView() {
        mHeaderView.setVisibility(View.VISIBLE);
    }

    void hideHeaderView() {
        mHeaderView.setVisibility(View.GONE);
    }

    void enableHeaderView(boolean enabled) {
        mNextPageButton.setEnabled(enabled);
    }

    protected void updateUI() {
        boolean threadEnabled = (threadId > 0);

        if (mDeleteThreadMenu != null) {
            mDeleteThreadMenu.setEnabled(threadEnabled);
        }

        if (mToggleEncryptionMenu != null) {
            Context context = getActivity();
            if (context != null) {
                if (mConversation != null && Preferences.getEncryptionEnabled(context)) {
                    boolean encryption = mConversation.isEncryptionEnabled();
                    mToggleEncryptionMenu.setVisible(true).setEnabled(true).setChecked(encryption);
                } else {
                    mToggleEncryptionMenu.setVisible(false).setEnabled(false).setChecked(false);
                }
            }
        }
    }

    boolean tryHideEmojiDrawer() {
        if (mComposer.isEmojiVisible()) {
            mComposer.hideEmojiDrawer(false);
            return true;
        }
        return false;
    }

    public Conversation getConversation() {
        return mConversation;
    }

    public Contact getContact() {
        return (mConversation != null) ? mConversation.getContact() : null;
    }

    public long getThreadId() {
        return threadId;
    }

    protected void setThreadId(long threadId) {
        this.threadId = threadId;
    }

    /** Returns the user id of this conversation. */
    public abstract String getUserId();

    public void setTextEntry(CharSequence text) {
        mComposer.setText(text);
    }

    @Override
    public boolean onLongClick(View v) {
        // this seems to be necessary...
        return false;
    }

    public void closeConversation() {
        // main activity
        if (getParentActivity() != null) {
            getActivity().finish();
        }
        // using fragments...
        else {
            ConversationsActivity activity = (ConversationsActivity) getActivity();
            activity.getListFragment().endConversation(this);
        }
    }

    private void offlineModeWarning() {
        if (Preferences.getOfflineMode() && !mOfflineModeWarned) {
            mOfflineModeWarned = true;
            Toast.makeText(getActivity(), R.string.warning_offline_mode, Toast.LENGTH_LONG).show();
        }
    }

    @Override
    public void textChanged(CharSequence text) {
        Snackbar bar = SnackbarManager.getCurrentSnackbar();
        if (bar != null) {
            WarningType type = (WarningType) bar.getTag();
            if (type != null && type.getValue() < WarningType.FATAL.getValue()) {
                bar.dismiss();
            }
        }
    }

    @Override
    public void onRecordingSuccessful(File file) {
        if (file != null)
            sendBinaryMessage(Uri.fromFile(file), AudioDialog.DEFAULT_MIME, false, AudioComponent.class);
    }

    @Override
    public void onRecordingCancel() {
        mAudioDialog = null;
    }

    @Override
    public void buttonClick(File audioFile, AudioContentViewControl view, long messageId) {
        AudioFragment audio = getAudioFragment();
        if (audio.getMessageId() == messageId) {
            switch (mMediaPlayerStatus) {
            case AudioContentView.STATUS_PLAYING:
                pauseAudio(view);
                break;
            case AudioContentView.STATUS_PAUSED:
            case AudioContentView.STATUS_ENDED:
                playAudio(view, messageId);
                break;

            }
        } else {
            switch (mMediaPlayerStatus) {
            case AudioContentView.STATUS_IDLE:
                if (prepareAudio(audioFile, view, messageId))
                    playAudio(view, messageId);
                break;
            case AudioContentView.STATUS_ENDED:
            case AudioContentView.STATUS_PLAYING:
            case AudioContentView.STATUS_PAUSED:
                resetAudio(mAudioControl);
                if (prepareAudio(audioFile, view, messageId))
                    playAudio(view, messageId);
                break;
            }
        }
    }

    private boolean prepareAudio(File audioFile, final AudioContentViewControl view, final long messageId) {
        stopMediaPlayerUpdater();
        try {
            AudioFragment audio = getAudioFragment();
            final MediaPlayer player = audio.getPlayer();
            player.setAudioStreamType(AudioManager.STREAM_MUSIC);
            player.setDataSource(audioFile.getAbsolutePath());
            player.prepare();

            // prepare was successful
            audio.setMessageId(messageId);
            mAudioControl = view;

            view.prepare(player.getDuration());
            player.seekTo(view.getPosition());
            view.setProgressChangeListener(true);
            player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {
                    stopMediaPlayerUpdater();
                    view.end();
                    AudioFragment audio = getAudioFragment();
                    if (audio != null) {
                        // this is mainly to get the wake lock released
                        audio.pausePlaying();
                        audio.seekPlayerTo(0);
                    }
                    setAudioStatus(AudioContentView.STATUS_ENDED);
                }
            });
            return true;
        } catch (IOException e) {
            Toast.makeText(getActivity(), R.string.err_file_not_found, Toast.LENGTH_SHORT).show();
            return false;
        }
    }

    @Override
    public void playAudio(AudioContentViewControl view, long messageId) {
        view.play();
        getAudioFragment().startPlaying();
        setAudioStatus(AudioContentView.STATUS_PLAYING);
        startMediaPlayerUpdater(view);
    }

    private void updatePosition(AudioContentViewControl view) {
        // we don't use getElapsedTime() here because it might get moved by seeking
        view.updatePosition(getAudioFragment().getPlayer().getCurrentPosition());
    }

    @Override
    public void pauseAudio(AudioContentViewControl view) {
        view.pause();
        getAudioFragment().pausePlaying();
        stopMediaPlayerUpdater();
        setAudioStatus(AudioContentView.STATUS_PAUSED);
    }

    private void resetAudio(AudioContentViewControl view) {
        if (view != null) {
            stopMediaPlayerUpdater();
            view.end();
        }
        AudioFragment audio = getAudioFragment();
        if (audio != null) {
            audio.resetPlayer();
            audio.setMessageId(-1);
        }
    }

    private void setAudioStatus(int audioStatus) {
        mMediaPlayerStatus = audioStatus;
    }

    @Override
    public void stopAllSounds() {
        resetAudio(mAudioControl);
    }

    @Override
    public void onBind(long messageId, final AudioContentViewControl view) {
        final AudioFragment audio = getAudioFragment();
        if (audio != null && audio.getMessageId() == messageId) {
            mAudioControl = view;
            audio.getPlayer().setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {
                    stopMediaPlayerUpdater();
                    view.end();
                    audio.seekPlayerTo(0);
                    setAudioStatus(AudioContentView.STATUS_ENDED);
                }
            });

            view.setProgressChangeListener(true);
            view.prepare(audio.getPlayer().getDuration());
            if (audio.isPlaying()) {
                startMediaPlayerUpdater(view);
                view.play();
            } else {
                view.pause();
            }
        }
    }

    @Override
    public void onUnbind(long messageId, AudioContentViewControl view) {
        AudioFragment audio = getAudioFragment();
        if (audio != null && audio.getMessageId() == messageId) {
            mAudioControl = null;
            MediaPlayer player = audio.getPlayer();
            player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {
                    AudioFragment audio = getAudioFragment();
                    if (audio != null)
                        audio.seekPlayerTo(0);
                    setAudioStatus(AudioContentView.STATUS_ENDED);
                }
            });

            view.setProgressChangeListener(false);
            if (!MessagesProviderUtils.exists(getActivity(), messageId)) {
                resetAudio(view);
            }

            else {
                stopMediaPlayerUpdater();
            }
        }
    }

    @Override
    public boolean isPlaying() {
        AudioFragment audio = getAudioFragment();
        return audio != null && audio.isPlaying();
    }

    @Override
    public void seekTo(int position) {
        AudioFragment audio = getAudioFragment();
        if (audio != null)
            audio.seekPlayerTo(position);
    }

    private void startMediaPlayerUpdater(final AudioContentViewControl view) {
        updatePosition(view);
        mMediaPlayerUpdater = new Runnable() {
            @Override
            public void run() {
                updatePosition(view);
                mHandler.postDelayed(this, 100);
            }
        };
        mHandler.postDelayed(mMediaPlayerUpdater, 100);
    }

    private void stopMediaPlayerUpdater() {
        if (mMediaPlayerUpdater != null) {
            mHandler.removeCallbacks(mMediaPlayerUpdater);
            mMediaPlayerUpdater = null;
        }
    }

    /** The conversation list query handler. */
    private static final class MessageListQueryHandler extends AsyncQueryHandler {
        private WeakReference<AbstractComposeFragment> mParent;
        private boolean mCancel;
        private long mLastId;

        MessageListQueryHandler(AbstractComposeFragment parent) {
            super(parent.getActivity().getApplicationContext().getContentResolver());
            mParent = new WeakReference<>(parent);
        }

        @Override
        public synchronized void startQuery(int token, Object cookie, Uri uri, String[] projection,
                String selection, String[] selectionArgs, String orderBy) {
            mCancel = false;
            super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
        }

        @Override
        protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
            final AbstractComposeFragment parent = mParent.get();
            if (parent == null || cursor == null || parent.isFinishing() || mCancel) {
                // close cursor - if any
                if (cursor != null)
                    cursor.close();

                mCancel = false;
                if (parent != null) {
                    parent.unregisterPeerObserver();
                    parent.mListAdapter.changeCursor(null);
                }
                return;
            }

            switch (token) {
            case MESSAGE_LIST_QUERY_TOKEN:

                // no messages to show - exit
                if (cursor.getCount() == 0 && (parent.mConversation == null ||
                // no draft
                        (parent.mConversation.getDraft() == null &&
                        // no subscription request
                                parent.mConversation.getRequestStatus() != Threads.REQUEST_WAITING &&
                                // no text in compose entry
                                parent.mComposer.getText().length() == 0 &&
                                // no group chat
                                !parent.mConversation.isGroupChat()))) {

                    Log.i(TAG, "no data to view - exit");

                    // close conversation
                    parent.closeConversation();

                } else {
                    // first query - use last id of this new cursor
                    if (cursor.getCount() > 0) {
                        cursor.moveToFirst();
                        mLastId = Conversation.getMessageId(cursor);
                    }

                    // save reloading status for next time
                    Bundle args = parent.getArguments();

                    // see if we have to scroll to a specific message
                    int newSelectionPos = -1;

                    if (args != null && !args.getBoolean(ComposeMessage.EXTRA_RELOADING)) {
                        long msgId = args.getLong(ComposeMessage.EXTRA_MESSAGE, -1);
                        if (msgId > 0) {
                            cursor.moveToPosition(-1);
                            while (cursor.moveToNext()) {
                                long curId = cursor.getLong(CompositeMessage.COLUMN_ID);
                                if (curId == msgId) {
                                    newSelectionPos = cursor.getPosition();
                                    break;
                                }
                            }
                        }

                        args.putBoolean(ComposeMessage.EXTRA_RELOADING, true);
                    }

                    parent.mListAdapter.changeCursor(cursor);
                    if (newSelectionPos >= 0) {
                        // +1 is for the header view
                        final int pos = newSelectionPos + 1;
                        parent.getListView().post(new Runnable() {
                            @Override
                            public void run() {
                                parent.scrollToPosition(pos);
                            }
                        });
                    }

                    if (newSelectionPos < 0 && cursor.getCount() >= MESSAGE_PAGE_SIZE)
                        parent.showHeaderView();

                    parent.updateUI();
                }

                break;

            case MESSAGE_PAGE_QUERY_TOKEN:
                if (cursor.getCount() > 0) {
                    int newSelectionPos = -1;

                    // there is no more data after this page
                    if (cursor.getCount() < MESSAGE_PAGE_SIZE)
                        parent.hideHeaderView();

                    // save last id of this new cursor
                    cursor.moveToFirst();
                    mLastId = Conversation.getMessageId(cursor);

                    // join with the old cursor (if any)
                    Cursor oldCursor = parent.mListAdapter.getCursor();
                    if (oldCursor != null) {
                        // the new selection will be the next item after this new cursor
                        newSelectionPos = cursor.getCount();
                        cursor = new MergeCursor(new Cursor[] { cursor, oldCursor });
                    }

                    parent.mListAdapter.swapCursor(cursor);
                    if (newSelectionPos >= 0)
                        parent.getListView().setSelection(newSelectionPos);

                    parent.updateUI();
                } else {
                    // this happens when the first page is exactly PAGE_SIZE big
                    parent.hideHeaderView();
                }

                parent.enableHeaderView(true);
                break;

            case CONVERSATION_QUERY_TOKEN:
                if (cursor.moveToFirst()) {
                    parent.mConversation = Conversation.createFromCursor(parent.getActivity(), cursor);
                    parent.onConversationCreated();
                }

                cursor.close();

                parent.startMessagesQuery();
                break;

            default:
                Log.e(TAG, "onQueryComplete called with unknown token " + token);
            }

        }

        public synchronized void abort() {
            mCancel = true;
            mLastId = 0;
            cancelOperation(MESSAGE_LIST_QUERY_TOKEN);
            cancelOperation(CONVERSATION_QUERY_TOKEN);
            cancelOperation(MESSAGE_PAGE_QUERY_TOKEN);
        }

        public long getLastId() {
            return mLastId;
        }

    }

}